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 unquote as urlunquote
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,68 @@ 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"
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+ url_str = resource_url_or_path
136+ # Strip the scheme prefix to get the path portion
137+ _ , path_part = url_str .split ("://" , 1 )
138+ decoded = urlunquote (path_part )
139+ self ._path = Path (decoded ).absolute ()
145140 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 ()
170- )
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 )
175-
176- self .fs = fs
177- self .filename = filename
141+ self ._path = Path (resource_url_or_path ).absolute ()
142+
178143 return self
179144
180- exists = proxy ("exists" )
181- open = proxy ("open" )
182- unlink = proxy ("remove" )
183- rmdir = proxy ("removedir" )
184- removetree = proxy ("removetree" )
185- geturl = proxy ("geturl" )
145+ def exists (self ):
146+ return os .path .exists (self ._path )
186147
187- def getsyspath (self ):
188- return Path ( os . fsdecode ( self .fs . getsyspath ( self . filename )) )
148+ def open (self , mode = "r" , ** kw ):
149+ return self ._path . open ( mode , ** kw )
189150
190- def joinpath (self , other ):
191- """Create a new FSResource based on an existing one
151+ def unlink (self ):
152+ self . _path . unlink ()
192153
193- Note that calling .close() on either one (or exiting the
194- context of the original) will close the filesystem that both use.
154+ def rmdir ( self ):
155+ self . _path . rmdir ()
195156
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 ))
157+ def removetree (self ):
158+ shutil .rmtree (self ._path )
201159
202- def copy_to (self , other ):
203- """Create a new FSResource by copying the underlying resource
160+ def getsyspath (self ):
161+ return self . _path
204162
205- Note that calling .close() on either one (or exiting the
206- context of the original) will close the filesystem that both use.
163+ def geturl ( self ):
164+ return f"file:// { urllib . request . pathname2url ( str ( self . _path )) } "
207165
208- In practice, if you use the new one within the open context
209- of the old one, you'll be fine.
210- """
166+ def joinpath (self , other ):
167+ return FSResource .new (self ._path / other )
168+
169+ def copy_to (self , other ):
211170 if isinstance (other , (str , Path )):
212171 other = FSResource .new (other )
213- copy . copy_file (self .fs , self . filename , other .fs , other . filename )
172+ shutil . copy2 (self ._path , other ._path )
214173
215174 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 )
175+ try :
176+ self ._path .mkdir (parents = parents , exist_ok = exist_ok )
177+ except FileExistsError :
178+ if not exist_ok :
179+ raise
220180
221181 def __contains__ (self , other ):
222- return other in str (self .geturl () )
182+ return other in str (self ._path )
223183
224184 @property
225185 def suffix (self ):
226- return Path ( self ) .suffix
186+ return self . _path .suffix
227187
228188 def __truediv__ (self , other ):
229189 return self .joinpath (other )
@@ -232,31 +192,24 @@ def __repr__(self):
232192 return f"<FSResource { self .geturl ()} >"
233193
234194 def __str__ (self ):
235- rc = self .geturl ()
236- if rc .startswith ("file://" ):
237- return rc [6 :]
195+ return str (self ._path )
238196
239197 def __fspath__ (self ):
240- return self . fs . getsyspath (self .filename )
198+ return str (self ._path )
241199
242200 def close (self ):
243- self . fs . close ()
201+ pass # no-op: no filesystem to close
244202
245203 @staticmethod
246204 @contextmanager
247205 def open_fs_resource (
248- resource_url_or_path : Union [str , Path , "FSResource" ], filesystem : base . FS = None
206+ resource_url_or_path : Union [str , Path , "FSResource" ],
249207 ):
250208 """Create a context-managed FSResource
251209
252210 Input is a URL, path (absolute or relative) or FSResource
253211
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.
212+ The function should be used in a context manager.
260213
261214 For example:
262215
@@ -278,13 +231,8 @@ def open_fs_resource(
278231 # yam
279232
280233 """
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 ()
234+ resource = FSResource .new (resource_url_or_path )
235+ yield resource
288236
289237
290238open_fs_resource = FSResource .open_fs_resource
0 commit comments