11from __future__ import annotations
22
3+ import abc
4+ import os
5+ from os .path import expanduser
36from typing import (
47 Optional ,
58 Dict ,
1013 Mapping ,
1114 cast ,
1215 Protocol ,
16+ TypeVar ,
17+ Generic ,
18+ runtime_checkable ,
1319)
1420
21+ import fs .opener .errors
22+ import pkg_resources
1523from fs import ResourceType , errors
1624from fs .base import FS
1725from fs .info import Info
1826from fs .memoryfs import MemoryFS , _DirEntry , _MemoryFile
1927from fs .multifs import MultiFS
28+ from fs .opener import Opener , registry as fs_registry
29+ from fs .opener .parse import ParseResult
2030from fs .path import split
2131
22- from relic .sga .core .definitions import Version , MagicWord
32+ from relic .sga .core .definitions import Version , MagicWord , _validate_magic_word
2333from relic .sga .core .errors import VersionNotSupportedError
2434
2535ESSENCE_NAMESPACE = "essence"
2636
37+ TKey = TypeVar ("TKey" )
38+ TValue = TypeVar ("TValue" )
2739
40+
41+ class EntrypointRegistry (Generic [TKey , TValue ]):
42+ def __init__ (self , entry_point_path : str , autoload : bool = False ):
43+ self ._entry_point_path = entry_point_path
44+ self ._mapping : Dict [TKey , TValue ] = {}
45+ self ._autoload = autoload
46+
47+ def register (self , key : TKey , value : TValue ) -> None :
48+ self ._mapping [key ] = value
49+
50+ @abc .abstractmethod
51+ def auto_register (self , value : TValue ) -> None :
52+ raise NotImplementedError
53+
54+ def get (self , key : TKey , default : Optional [TValue ] = None ) -> Optional [TValue ]:
55+ if key in self ._mapping :
56+ return self ._mapping [key ]
57+
58+ if self ._autoload :
59+ try :
60+ entry_point = next (
61+ pkg_resources .iter_entry_points (
62+ self ._entry_point_path , self ._key2entry_point_path (key )
63+ )
64+ )
65+ except StopIteration :
66+ entry_point = None
67+ if entry_point is None :
68+ return default
69+ self ._auto_register_entrypoint (entry_point )
70+ if key not in self ._mapping :
71+ raise NotImplementedError # TODO specify autoload failed to load in a usable value
72+ return self ._mapping [key ]
73+ return default
74+
75+ @abc .abstractmethod
76+ def _key2entry_point_path (self , key : TKey ) -> str :
77+ raise NotImplementedError
78+
79+ def _auto_register_entrypoint (self , entry_point : Any ) -> None :
80+ try :
81+ entry_point_result = entry_point .load ()
82+ except : # Wrap in exception
83+ raise
84+ return self ._register_entrypoint (entry_point_result )
85+
86+ @abc .abstractmethod
87+ def _register_entrypoint (self , entry_point_result : Any ) -> None :
88+ raise NotImplementedError
89+
90+
91+ @runtime_checkable
2892class EssenceFSHandler (Protocol ):
93+ version : Version
94+
2995 def read (self , stream : BinaryIO ) -> EssenceFS :
3096 raise NotImplementedError
3197
3298 def write (self , stream : BinaryIO , essence_fs : EssenceFS ) -> int :
3399 raise NotImplementedError
34100
35101
36- class EssenceFSFactory :
37- def __init__ (self ) -> None :
38- self . handler_map : Dict [ Version , EssenceFSHandler ] = {}
102+ class EssenceFSFactory ( EntrypointRegistry [ Version , EssenceFSHandler ]) :
103+ def _key2entry_point_path (self , key : Version ) -> str :
104+ return f"v { key . major } . { key . minor } "
39105
40- def register_handler (self , version : Version , handler : EssenceFSHandler ) -> None :
41- if version is None :
42- raise ValueError
43- if handler is None :
44- raise ValueError
45- # self.default_handler = handler
46- # else:
47- self .handler_map [version ] = handler
106+ def _register_entrypoint (self , entry_point_result : Any ) -> None :
107+ if isinstance (entry_point_result , EssenceFSHandler ):
108+ self .auto_register (entry_point_result )
109+ elif isinstance (entry_point_result , (list , tuple , Collection )):
110+ version , handler = entry_point_result
111+ if not isinstance (handler , EssenceFSHandler ):
112+ handler = handler ()
113+ self .register (version , handler )
114+ else :
115+ # Callable; register nested result
116+ self ._register_entrypoint (entry_point_result ())
117+
118+ def auto_register (self , value : EssenceFSHandler ) -> None :
119+ self .register (value .version , value )
120+
121+ def __init__ (self , autoload : bool = True ) -> None :
122+ super ().__init__ ("relic.sga.handler" , autoload )
48123
49124 @staticmethod
50125 def _read_magic_and_version (sga_stream : BinaryIO ) -> Version :
51- sga_stream .seek (0 )
52- MagicWord .read_magic_word (sga_stream )
53- return Version .unpack (sga_stream )
126+ # sga_stream.seek(0)
127+ jump_back = sga_stream .tell ()
128+ _validate_magic_word (MagicWord , sga_stream , advance = True )
129+ version = Version .unpack (sga_stream )
130+ sga_stream .seek (jump_back )
131+ return version
54132
55133 def _get_handler (self , version : Version ) -> EssenceFSHandler :
56- handler = self .handler_map . get (version )
134+ handler = self .get (version )
57135 if handler is None :
58136 # This may raise a 'false positive' if a Null handler is registered
59- raise VersionNotSupportedError (version , list (self .handler_map .keys ()))
137+ raise VersionNotSupportedError (version , list (self ._mapping .keys ()))
60138 return handler
61139
62140 def _get_handler_from_stream (
@@ -87,6 +165,55 @@ def write(
87165 return handler .write (sga_stream , sga_fs )
88166
89167
168+ registry = EssenceFSFactory (True )
169+
170+
171+ # @fs_registry.install
172+ # Can't use decorator; it breaks subclassing for entrypoints
173+ class EssenceFSOpener (Opener ):
174+ def __init__ (self , factory : Optional [EssenceFSFactory ] = None ):
175+ if factory is None :
176+ factory = registry
177+ self .factory = factory
178+
179+ protocols = ["sga" ]
180+
181+ def open_fs (
182+ self ,
183+ fs_url : str ,
184+ parse_result : ParseResult ,
185+ writeable : bool ,
186+ create : bool ,
187+ cwd : str ,
188+ ) -> FS :
189+ # All EssenceFS should be writable; so we can ignore that
190+
191+ # Resolve Path
192+ if fs_url == "sga://" :
193+ if create :
194+ return EssenceFS ()
195+ else :
196+ raise fs .opener .errors .OpenerError (
197+ "No path was given and opener not marked for 'create'!"
198+ )
199+
200+ _path = os .path .abspath (os .path .join (cwd , expanduser (parse_result .resource )))
201+ path = os .path .normpath (_path )
202+
203+ # Create will always create a new EssenceFS if needed
204+ try :
205+ with open (path , "rb" ) as sga_file :
206+ return self .factory .read (sga_file )
207+ except FileNotFoundError as e :
208+ if create :
209+ return EssenceFS ()
210+ else :
211+ raise
212+
213+
214+ fs_registry .install (EssenceFSOpener )
215+
216+
90217class _EssenceFile (_MemoryFile ):
91218 ... # I plan on allowing lazy file loading from the archive; I'll likely need to implement this to do that
92219
@@ -111,14 +238,30 @@ def to_info(self, namespaces=None):
111238
112239
113240class _EssenceDriveFS (MemoryFS ):
114- def __init__ (self ) -> None :
241+ def __init__ (self , alias : str ) -> None :
115242 super ().__init__ ()
243+ self .alias = alias
116244
117245 def _make_dir_entry (
118246 self , resource_type : ResourceType , name : str
119247 ) -> _EssenceDirEntry :
120248 return _EssenceDirEntry (resource_type , name )
121249
250+ def validatepath (self , path : str ) -> str :
251+ if ":" in path :
252+ parts = path .split (":" , 1 )
253+ if parts [0 ][0 ] == "/" :
254+ parts [0 ] = parts [0 ][1 :]
255+ if parts [0 ] != self .alias :
256+ raise fs .errors .InvalidPath (
257+ path ,
258+ f"Alias `{ parts [0 ]} ` does not math the Drive's Alias `{ self .alias } `" ,
259+ )
260+ fixed_path = parts [1 ]
261+ else :
262+ fixed_path = path
263+ return super ().validatepath (fixed_path )
264+
122265 def setinfo (self , path : str , info : Mapping [str , Mapping [str , object ]]) -> None :
123266 _path = self .validatepath (path )
124267 with self ._lock :
@@ -171,7 +314,7 @@ def getessence(self, path: str) -> Info:
171314 return self .getinfo (path , [ESSENCE_NAMESPACE ])
172315
173316 def create_drive (self , name : str ) -> _EssenceDriveFS :
174- drive = _EssenceDriveFS ()
317+ drive = _EssenceDriveFS (name )
175318 self .add_fs (name , drive )
176319 return drive
177320
@@ -227,4 +370,6 @@ def _delegate(self, path):
227370 "_EssenceDirEntry" ,
228371 "_EssenceDriveFS" ,
229372 "EssenceFS" ,
373+ "registry" ,
374+ "EssenceFSOpener" ,
230375]
0 commit comments