11import os
2+ import shutil
23import urllib .request
34import webbrowser
45from contextlib import contextmanager
56from io import StringIO , TextIOWrapper
67from pathlib import Path
78from typing import IO , ContextManager , Text , Tuple , Union
9+ from urllib .parse import urlparse
810
911import requests
10- from fs import base , copy , open_fs
11- from fs import path as fspath
1212
1313"""Utilities for working with files"""
1414
@@ -94,14 +94,6 @@ def load_from_source(source: DataInput) -> ContextManager[Tuple[IO[Text], Text]]
9494 yield f , path
9595
9696
97- def proxy (funcname ):
98- def func (self , * args , ** kwargs ):
99- real_func = getattr (self .fs , funcname )
100- return real_func (self .filename , * args , ** kwargs )
101-
102- return func
103-
104-
10597def view_file (path ):
10698 """Open the given file in a webbrowser or whatever
10799
@@ -115,7 +107,7 @@ def view_file(path):
115107
116108
117109class FSResource :
118- """Generalization of pathlib.Path to support S3, FTP, etc
110+ """A pathlib.Path-based resource abstraction for local filesystem operations.
119111
120112 Create them through the open_fs_resource module function or static
121113 function which will create a context manager that generates an FSResource.
@@ -130,100 +122,72 @@ def __init__(self):
130122 def new (
131123 cls ,
132124 resource_url_or_path : Union [str , Path , "FSResource" ],
133- filesystem : base .FS = None ,
134125 ):
135126 """Directly create a new FSResource from a URL or path (absolute or relative)
136127
137128 You can call this to bypass the context manager in contexts where closing isn't
138129 important (e.g. interactive repl experiments)."""
139130 self = cls .__new__ (cls )
140131
141- if isinstance (resource_url_or_path , str ) and "://" in resource_url_or_path :
142- path_type = "url"
143- elif isinstance (resource_url_or_path , FSResource ):
144- path_type = "resource"
145- else :
146- resource_url_or_path = Path (resource_url_or_path )
147- path_type = "path"
148-
149- if filesystem :
150- assert path_type != "resource"
151- fs = filesystem
152- filename = str (resource_url_or_path )
153- elif path_type == "resource" : # clone a resource reference
154- fs = resource_url_or_path .fs
155- filename = resource_url_or_path .filename
156- elif path_type == "path" :
157- if resource_url_or_path .is_absolute ():
158- if resource_url_or_path .drive :
159- root = resource_url_or_path .drive + "/"
160- else :
161- root = resource_url_or_path .root
162- filename = resource_url_or_path .relative_to (root ).as_posix ()
163- else :
164- root = Path ("/" ).absolute ()
165- filename = (
166- (Path ("." ) / resource_url_or_path )
167- .absolute ()
168- .relative_to (root )
169- .as_posix ()
132+ if isinstance (resource_url_or_path , FSResource ):
133+ self ._path = resource_url_or_path ._path
134+ elif isinstance (resource_url_or_path , str ) and "://" in resource_url_or_path :
135+ parsed = urlparse (resource_url_or_path )
136+ if parsed .scheme != "file" :
137+ raise ValueError (
138+ f"Only file:// URLs are supported, got { parsed .scheme } ://"
170139 )
171- fs = open_fs (str (root ))
172- elif path_type == "url" :
173- path , filename = resource_url_or_path .replace ("\\ " , "/" ).rsplit ("/" , 1 )
174- fs = open_fs (path )
140+ # For relative file URLs like file://relative/path, urlparse puts
141+ # "relative" in netloc and "/path" in path. Concatenate them.
142+ if parsed .netloc and parsed .netloc .lower () != "localhost" :
143+ path_str = parsed .netloc + parsed .path
144+ else :
145+ path_str = parsed .path
146+ decoded = urllib .request .url2pathname (path_str )
147+ self ._path = Path (decoded ).absolute ()
148+ else :
149+ self ._path = Path (resource_url_or_path ).absolute ()
175150
176- self .fs = fs
177- self .filename = filename
178151 return self
179152
180- exists = proxy ("exists" )
181- open = proxy ("open" )
182- unlink = proxy ("remove" )
183- rmdir = proxy ("removedir" )
184- removetree = proxy ("removetree" )
185- geturl = proxy ("geturl" )
153+ def exists (self ):
154+ return os .path .exists (self ._path )
186155
187- def getsyspath (self ):
188- return Path ( os . fsdecode ( self .fs . getsyspath ( self . filename )) )
156+ def open (self , mode = "r" , ** kw ):
157+ return self ._path . open ( mode , ** kw )
189158
190- def joinpath (self , other ):
191- """Create a new FSResource based on an existing one
159+ def unlink (self ):
160+ self . _path . unlink ()
192161
193- Note that calling .close() on either one (or exiting the
194- context of the original) will close the filesystem that both use.
162+ def rmdir ( self ):
163+ self . _path . rmdir ()
195164
196- In practice, if you use the new one within the open context
197- of the old one, you'll be fine.
198- """
199- path = fspath .join (self .filename , other )
200- return FSResource .new (self .fs .geturl (path ))
165+ def removetree (self ):
166+ shutil .rmtree (self ._path )
201167
202- def copy_to (self , other ):
203- """Create a new FSResource by copying the underlying resource
168+ def getsyspath (self ):
169+ return self . _path
204170
205- Note that calling .close() on either one (or exiting the
206- context of the original) will close the filesystem that both use.
171+ def geturl ( self ):
172+ return f"file:// { urllib . request . pathname2url ( str ( self . _path )) } "
207173
208- In practice, if you use the new one within the open context
209- of the old one, you'll be fine.
210- """
174+ def joinpath (self , other ):
175+ return FSResource .new (self ._path / other )
176+
177+ def copy_to (self , other ):
211178 if isinstance (other , (str , Path )):
212179 other = FSResource .new (other )
213- copy . copy_file (self .fs , self . filename , other .fs , other . filename )
180+ shutil . copy2 (self ._path , other ._path )
214181
215182 def mkdir (self , * , parents = False , exist_ok = False ):
216- if parents :
217- self .fs .makedirs (self .filename , recreate = exist_ok )
218- else :
219- self .fs .makedir (self .filename , recreate = exist_ok )
183+ self ._path .mkdir (parents = parents , exist_ok = exist_ok )
220184
221185 def __contains__ (self , other ):
222- return other in str (self .geturl () )
186+ return str ( other ) in str (self ._path )
223187
224188 @property
225189 def suffix (self ):
226- return Path ( self ) .suffix
190+ return self . _path .suffix
227191
228192 def __truediv__ (self , other ):
229193 return self .joinpath (other )
@@ -232,31 +196,24 @@ def __repr__(self):
232196 return f"<FSResource { self .geturl ()} >"
233197
234198 def __str__ (self ):
235- rc = self .geturl ()
236- if rc .startswith ("file://" ):
237- return rc [6 :]
199+ return str (self ._path )
238200
239201 def __fspath__ (self ):
240- return self . fs . getsyspath (self .filename )
202+ return str (self ._path )
241203
242204 def close (self ):
243- self . fs . close ()
205+ pass # no-op: no filesystem to close
244206
245207 @staticmethod
246208 @contextmanager
247209 def open_fs_resource (
248- resource_url_or_path : Union [str , Path , "FSResource" ], filesystem : base . FS = None
210+ resource_url_or_path : Union [str , Path , "FSResource" ],
249211 ):
250212 """Create a context-managed FSResource
251213
252214 Input is a URL, path (absolute or relative) or FSResource
253215
254- The function should be used in a context manager. The
255- resource's underlying filesystem will be closed automatically
256- when the context ends and the data will be saved back to the
257- filesystem (local, remote, zipfile, etc.)
258-
259- Think of it as a way of "mounting" a filesystem, directory or file.
216+ The function should be used in a context manager.
260217
261218 For example:
262219
@@ -278,13 +235,8 @@ def open_fs_resource(
278235 # yam
279236
280237 """
281- resource = FSResource .new (resource_url_or_path , filesystem )
282- if not filesystem :
283- filesystem = resource
284- try :
285- yield resource
286- finally :
287- filesystem .close ()
238+ resource = FSResource .new (resource_url_or_path )
239+ yield resource
288240
289241
290242open_fs_resource = FSResource .open_fs_resource
0 commit comments