1+ """upath.registry -- registry for file system specific implementations
2+
3+ Retrieve UPath implementations via `get_upath_class`.
4+ Register custom UPath subclasses in one of two ways:
5+
6+ ### directly from Python
7+
8+ >>> from upath import UPath
9+ >>> from upath.registry import register_implementation
10+ >>> my_protocol = "myproto"
11+ >>> class MyPath(UPath):
12+ ... pass
13+ >>> register_implementation(my_protocol, MyPath)
14+
15+ ### via entry points
16+
17+ ```toml
18+ # pyproject.toml
19+ [project.entry-points."unversal_pathlib.implementations"]
20+ myproto = "my_module.submodule:MyPath"
21+ ```
22+
23+ ```ini
24+ # setup.cfg
25+ [options.entry_points]
26+ universal_pathlib.implementations =
27+ myproto = my_module.submodule:MyPath
28+ ```
29+ """
130from __future__ import annotations
231
3- import importlib
432import os
33+ import re
34+ import sys
535import warnings
36+ from collections import ChainMap
637from functools import lru_cache
7- from typing import TYPE_CHECKING
38+ from importlib import import_module
39+ from importlib .metadata import entry_points
40+ from typing import Iterator
41+ from typing import MutableMapping
842
943from fsspec .core import get_filesystem_class
44+ from fsspec .registry import available_protocols
1045
11- if TYPE_CHECKING :
12- from upath .core import UPath
46+ import upath .core
1347
1448__all__ = [
1549 "get_upath_class" ,
50+ "available_implementations" ,
51+ "register_implementation" ,
1652]
1753
1854
19- class _Registry :
55+ _ENTRY_POINT_GROUP = "universal_pathlib.implementations"
56+
57+
58+ class _Registry (MutableMapping [str , "type[upath.core.UPath]" ]):
59+ """internal registry for UPath subclasses"""
60+
2061 known_implementations : dict [str , str ] = {
2162 "abfs" : "upath.implementations.cloud.AzurePath" ,
2263 "abfss" : "upath.implementations.cloud.AzurePath" ,
@@ -35,26 +76,118 @@ class _Registry:
3576 "webdav+https" : "upath.implementations.webdav.WebdavPath" ,
3677 }
3778
38- def __getitem__ (self , item : str ) -> type [UPath ] | None :
39- try :
40- fqn = self .known_implementations [item ]
41- except KeyError :
42- return None
43- module_name , name = fqn .rsplit ("." , 1 )
44- mod = importlib .import_module (module_name )
45- return getattr (mod , name ) # type: ignore
79+ def __init__ (self ) -> None :
80+ if sys .version_info >= (3 , 10 ):
81+ eps = entry_points (group = _ENTRY_POINT_GROUP )
82+ else :
83+ eps = entry_points ().get (_ENTRY_POINT_GROUP , [])
84+ self ._entries = {ep .name : ep for ep in eps }
85+ self ._m = ChainMap ({}, self .known_implementations ) # type: ignore
86+
87+ def __contains__ (self , item : object ) -> bool :
88+ return item in set ().union (self ._m , self ._entries )
89+
90+ def __getitem__ (self , item : str ) -> type [upath .core .UPath ]:
91+ fqn = self ._m .get (item )
92+ if fqn is None :
93+ if item in self ._entries :
94+ fqn = self ._m [item ] = self ._entries [item ].load ()
95+ if fqn is None :
96+ raise KeyError (f"{ item } not in registry" )
97+ if isinstance (fqn , str ):
98+ module_name , name = fqn .rsplit ("." , 1 )
99+ mod = import_module (module_name )
100+ cls = getattr (mod , name ) # type: ignore
101+ else :
102+ cls = fqn
103+ return cls
104+
105+ def __setitem__ (self , item : str , value : type [upath .core .UPath ] | str ) -> None :
106+ if not (
107+ (isinstance (value , type ) and issubclass (value , upath .core .UPath ))
108+ or isinstance (value , str )
109+ ):
110+ raise ValueError (
111+ f"expected UPath subclass or FQN-string, got: { type (value ).__name__ !r} "
112+ )
113+ self ._m [item ] = value
114+
115+ def __delitem__ (self , __v : str ) -> None :
116+ raise NotImplementedError ("removal is unsupported" )
117+
118+ def __len__ (self ) -> int :
119+ return len (set ().union (self ._m , self ._entries ))
120+
121+ def __iter__ (self ) -> Iterator [str ]:
122+ return iter (set ().union (self ._m , self ._entries ))
46123
47124
48125_registry = _Registry ()
49126
50127
51- @lru_cache
52- def get_upath_class (protocol : str ) -> type [UPath ] | None :
53- """Return the upath cls for the given protocol."""
54- cls : type [UPath ] | None = _registry [protocol ]
55- if cls is not None :
56- return cls
128+ def available_implementations (* , fallback : bool = False ) -> list [str ]:
129+ """return a list of protocols for available implementations
130+
131+ Parameters
132+ ----------
133+ fallback:
134+ If True, also return protocols for fsspec filesystems without
135+ an implementation in universal_pathlib.
136+ """
137+ impl = list (_registry )
138+ if not fallback :
139+ return impl
57140 else :
141+ return list ({* impl , * available_protocols ()})
142+
143+
144+ def register_implementation (
145+ protocol : str ,
146+ cls : type [upath .core .UPath ] | str ,
147+ * ,
148+ clobber : bool = False ,
149+ ) -> None :
150+ """register a UPath implementation with a protocol
151+
152+ Parameters
153+ ----------
154+ protocol:
155+ Protocol name to associate with the class
156+ cls:
157+ The UPath subclass for the protocol or a str representing the
158+ full path to an implementation class like package.module.class.
159+ clobber:
160+ Whether to overwrite a protocol with the same name; if False,
161+ will raise instead.
162+ """
163+ if not re .match (r"^[a-z][a-z0-9+_.]+$" , protocol ):
164+ raise ValueError (f"{ protocol !r} is not a valid URI scheme" )
165+ if not clobber and protocol in _registry :
166+ raise ValueError (f"{ protocol !r} is already in registry and clobber is False!" )
167+ _registry [protocol ] = cls
168+
169+
170+ @lru_cache
171+ def get_upath_class (
172+ protocol : str ,
173+ * ,
174+ fallback : bool = True ,
175+ ) -> type [upath .core .UPath ] | None :
176+ """Return the upath cls for the given protocol.
177+
178+ Returns `None` if no matching protocol can be found.
179+
180+ Parameters
181+ ----------
182+ protocol:
183+ The protocol string
184+ fallback:
185+ If fallback is False, don't return UPath instances for fsspec
186+ filesystems that don't have an implementation registered.
187+ """
188+ try :
189+ return _registry [protocol ]
190+ except KeyError :
58191 if not protocol :
59192 if os .name == "nt" :
60193 from upath .implementations .local import WindowsUPath
@@ -64,6 +197,8 @@ def get_upath_class(protocol: str) -> type[UPath] | None:
64197 from upath .implementations .local import PosixUPath
65198
66199 return PosixUPath
200+ if not fallback :
201+ return None
67202 try :
68203 _ = get_filesystem_class (protocol )
69204 except ValueError :
@@ -76,5 +211,4 @@ def get_upath_class(protocol: str) -> type[UPath] | None:
76211 UserWarning ,
77212 stacklevel = 2 ,
78213 )
79- mod = importlib .import_module ("upath.core" )
80- return mod .UPath # type: ignore
214+ return upath .core .UPath
0 commit comments