Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2 

3accessor.py contains base classes for implementing accessor properties 

4that can be mixed into or pinned onto other pandas classes. 

5 

6""" 

7from typing import FrozenSet, Set 

8import warnings 

9 

10from pandas.util._decorators import Appender 

11 

12 

13class DirNamesMixin: 

14 _accessors: Set[str] = set() 

15 _deprecations: FrozenSet[str] = frozenset() 

16 

17 def _dir_deletions(self): 

18 """ 

19 Delete unwanted __dir__ for this object. 

20 """ 

21 return self._accessors | self._deprecations 

22 

23 def _dir_additions(self): 

24 """ 

25 Add additional __dir__ for this object. 

26 """ 

27 rv = set() 

28 for accessor in self._accessors: 

29 try: 

30 getattr(self, accessor) 

31 rv.add(accessor) 

32 except AttributeError: 

33 pass 

34 return rv 

35 

36 def __dir__(self): 

37 """ 

38 Provide method name lookup and completion. 

39 

40 Notes 

41 ----- 

42 Only provide 'public' methods. 

43 """ 

44 rv = set(dir(type(self))) 

45 rv = (rv - self._dir_deletions()) | self._dir_additions() 

46 return sorted(rv) 

47 

48 

49class PandasDelegate: 

50 """ 

51 Abstract base class for delegating methods/properties. 

52 """ 

53 

54 def _delegate_property_get(self, name, *args, **kwargs): 

55 raise TypeError(f"You cannot access the property {name}") 

56 

57 def _delegate_property_set(self, name, value, *args, **kwargs): 

58 raise TypeError(f"The property {name} cannot be set") 

59 

60 def _delegate_method(self, name, *args, **kwargs): 

61 raise TypeError(f"You cannot call method {name}") 

62 

63 @classmethod 

64 def _add_delegate_accessors( 

65 cls, delegate, accessors, typ: str, overwrite: bool = False 

66 ): 

67 """ 

68 Add accessors to cls from the delegate class. 

69 

70 Parameters 

71 ---------- 

72 cls 

73 Class to add the methods/properties to. 

74 delegate 

75 Class to get methods/properties and doc-strings. 

76 accessors : list of str 

77 List of accessors to add. 

78 typ : {'property', 'method'} 

79 overwrite : bool, default False 

80 Overwrite the method/property in the target class if it exists. 

81 """ 

82 

83 def _create_delegator_property(name): 

84 def _getter(self): 

85 return self._delegate_property_get(name) 

86 

87 def _setter(self, new_values): 

88 return self._delegate_property_set(name, new_values) 

89 

90 _getter.__name__ = name 

91 _setter.__name__ = name 

92 

93 return property( 

94 fget=_getter, fset=_setter, doc=getattr(delegate, name).__doc__ 

95 ) 

96 

97 def _create_delegator_method(name): 

98 def f(self, *args, **kwargs): 

99 return self._delegate_method(name, *args, **kwargs) 

100 

101 f.__name__ = name 

102 f.__doc__ = getattr(delegate, name).__doc__ 

103 

104 return f 

105 

106 for name in accessors: 

107 

108 if typ == "property": 

109 f = _create_delegator_property(name) 

110 else: 

111 f = _create_delegator_method(name) 

112 

113 # don't overwrite existing methods/properties 

114 if overwrite or not hasattr(cls, name): 

115 setattr(cls, name, f) 

116 

117 

118def delegate_names(delegate, accessors, typ: str, overwrite: bool = False): 

119 """ 

120 Add delegated names to a class using a class decorator. This provides 

121 an alternative usage to directly calling `_add_delegate_accessors` 

122 below a class definition. 

123 

124 Parameters 

125 ---------- 

126 delegate : object 

127 The class to get methods/properties & doc-strings. 

128 accessors : Sequence[str] 

129 List of accessor to add. 

130 typ : {'property', 'method'} 

131 overwrite : bool, default False 

132 Overwrite the method/property in the target class if it exists. 

133 

134 Returns 

135 ------- 

136 callable 

137 A class decorator. 

138 

139 Examples 

140 -------- 

141 @delegate_names(Categorical, ["categories", "ordered"], "property") 

142 class CategoricalAccessor(PandasDelegate): 

143 [...] 

144 """ 

145 

146 def add_delegate_accessors(cls): 

147 cls._add_delegate_accessors(delegate, accessors, typ, overwrite=overwrite) 

148 return cls 

149 

150 return add_delegate_accessors 

151 

152 

153# Ported with modifications from xarray 

154# https://github.com/pydata/xarray/blob/master/xarray/core/extensions.py 

155# 1. We don't need to catch and re-raise AttributeErrors as RuntimeErrors 

156# 2. We use a UserWarning instead of a custom Warning 

157 

158 

159class CachedAccessor: 

160 """ 

161 Custom property-like object. 

162 

163 A descriptor for caching accessors. 

164 

165 Parameters 

166 ---------- 

167 name : str 

168 Namespace that will be accessed under, e.g. ``df.foo``. 

169 accessor : cls 

170 Class with the extension methods. 

171 

172 Notes 

173 ----- 

174 For accessor, The class's __init__ method assumes that one of 

175 ``Series``, ``DataFrame`` or ``Index`` as the 

176 single argument ``data``. 

177 """ 

178 

179 def __init__(self, name: str, accessor) -> None: 

180 self._name = name 

181 self._accessor = accessor 

182 

183 def __get__(self, obj, cls): 

184 if obj is None: 

185 # we're accessing the attribute of the class, i.e., Dataset.geo 

186 return self._accessor 

187 accessor_obj = self._accessor(obj) 

188 # Replace the property with the accessor object. Inspired by: 

189 # http://www.pydanny.com/cached-property.html 

190 # We need to use object.__setattr__ because we overwrite __setattr__ on 

191 # NDFrame 

192 object.__setattr__(obj, self._name, accessor_obj) 

193 return accessor_obj 

194 

195 

196def _register_accessor(name, cls): 

197 def decorator(accessor): 

198 if hasattr(cls, name): 

199 warnings.warn( 

200 f"registration of accessor {repr(accessor)} under name " 

201 f"{repr(name)} for type {repr(cls)} is overriding a preexisting" 

202 f"attribute with the same name.", 

203 UserWarning, 

204 stacklevel=2, 

205 ) 

206 setattr(cls, name, CachedAccessor(name, accessor)) 

207 cls._accessors.add(name) 

208 return accessor 

209 

210 return decorator 

211 

212 

213_doc = """ 

214Register a custom accessor on %(klass)s objects. 

215 

216Parameters 

217---------- 

218name : str 

219 Name under which the accessor should be registered. A warning is issued 

220 if this name conflicts with a preexisting attribute. 

221 

222Returns 

223------- 

224callable 

225 A class decorator. 

226 

227See Also 

228-------- 

229%(others)s 

230 

231Notes 

232----- 

233When accessed, your accessor will be initialized with the pandas object 

234the user is interacting with. So the signature must be 

235 

236.. code-block:: python 

237 

238 def __init__(self, pandas_object): # noqa: E999 

239 ... 

240 

241For consistency with pandas methods, you should raise an ``AttributeError`` 

242if the data passed to your accessor has an incorrect dtype. 

243 

244>>> pd.Series(['a', 'b']).dt 

245Traceback (most recent call last): 

246... 

247AttributeError: Can only use .dt accessor with datetimelike values 

248 

249Examples 

250-------- 

251 

252In your library code:: 

253 

254 import pandas as pd 

255 

256 @pd.api.extensions.register_dataframe_accessor("geo") 

257 class GeoAccessor: 

258 def __init__(self, pandas_obj): 

259 self._obj = pandas_obj 

260 

261 @property 

262 def center(self): 

263 # return the geographic center point of this DataFrame 

264 lat = self._obj.latitude 

265 lon = self._obj.longitude 

266 return (float(lon.mean()), float(lat.mean())) 

267 

268 def plot(self): 

269 # plot this array's data on a map, e.g., using Cartopy 

270 pass 

271 

272Back in an interactive IPython session: 

273 

274 >>> ds = pd.DataFrame({'longitude': np.linspace(0, 10), 

275 ... 'latitude': np.linspace(0, 20)}) 

276 >>> ds.geo.center 

277 (5.0, 10.0) 

278 >>> ds.geo.plot() 

279 # plots data on a map 

280""" 

281 

282 

283@Appender( 

284 _doc 

285 % dict( 

286 klass="DataFrame", others=("register_series_accessor, register_index_accessor") 

287 ) 

288) 

289def register_dataframe_accessor(name): 

290 from pandas import DataFrame 

291 

292 return _register_accessor(name, DataFrame) 

293 

294 

295@Appender( 

296 _doc 

297 % dict( 

298 klass="Series", others=("register_dataframe_accessor, register_index_accessor") 

299 ) 

300) 

301def register_series_accessor(name): 

302 from pandas import Series 

303 

304 return _register_accessor(name, Series) 

305 

306 

307@Appender( 

308 _doc 

309 % dict( 

310 klass="Index", others=("register_dataframe_accessor, register_series_accessor") 

311 ) 

312) 

313def register_index_accessor(name): 

314 from pandas import Index 

315 

316 return _register_accessor(name, Index)