Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/core/accessor.py: 66%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

92 statements  

1""" 

2 

3accessor.py contains base classes for implementing accessor properties 

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

5 

6""" 

7 

8from __future__ import annotations 

9 

10import functools 

11from typing import ( 

12 TYPE_CHECKING, 

13 final, 

14) 

15import warnings 

16 

17from pandas.util._decorators import ( 

18 set_module, 

19) 

20from pandas.util._exceptions import find_stack_level 

21 

22if TYPE_CHECKING: 

23 from collections.abc import Callable 

24 

25 from pandas._typing import TypeT 

26 

27 from pandas import Index 

28 from pandas.core.generic import NDFrame 

29 

30 

31class DirNamesMixin: 

32 _accessors: set[str] = set() 

33 _hidden_attrs: frozenset[str] = frozenset() 

34 

35 @final 

36 def _dir_deletions(self) -> set[str]: 

37 """ 

38 Delete unwanted __dir__ for this object. 

39 """ 

40 return self._accessors | self._hidden_attrs 

41 

42 def _dir_additions(self) -> set[str]: 

43 """ 

44 Add additional __dir__ for this object. 

45 """ 

46 return {accessor for accessor in self._accessors if hasattr(self, accessor)} 

47 

48 def __dir__(self) -> list[str]: 

49 """ 

50 Provide method name lookup and completion. 

51 

52 Notes 

53 ----- 

54 Only provide 'public' methods. 

55 """ 

56 rv = set(super().__dir__()) 

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

58 return sorted(rv) 

59 

60 

61class PandasDelegate: 

62 """ 

63 Abstract base class for delegating methods/properties. 

64 """ 

65 

66 def _delegate_property_get(self, name: str, *args, **kwargs): 

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

68 

69 def _delegate_property_set(self, name: str, value, *args, **kwargs) -> None: 

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

71 

72 def _delegate_method(self, name: str, *args, **kwargs): 

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

74 

75 @classmethod 

76 def _add_delegate_accessors( 

77 cls, 

78 delegate, 

79 accessors: list[str], 

80 typ: str, 

81 overwrite: bool = False, 

82 accessor_mapping: Callable[[str], str] = lambda x: x, 

83 raise_on_missing: bool = True, 

84 ) -> None: 

85 """ 

86 Add accessors to cls from the delegate class. 

87 

88 Parameters 

89 ---------- 

90 cls 

91 Class to add the methods/properties to. 

92 delegate 

93 Class to get methods/properties and docstrings. 

94 accessors : list of str 

95 List of accessors to add. 

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

97 overwrite : bool, default False 

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

99 accessor_mapping: Callable, default lambda x: x 

100 Callable to map the delegate's function to the cls' function. 

101 raise_on_missing: bool, default True 

102 Raise if an accessor does not exist on delegate. 

103 False skips the missing accessor. 

104 """ 

105 

106 def _create_delegator_property(name: str): 

107 def _getter(self): 

108 return self._delegate_property_get(name) 

109 

110 def _setter(self, new_values): 

111 return self._delegate_property_set(name, new_values) 

112 

113 _getter.__name__ = name 

114 _setter.__name__ = name 

115 

116 return property( 

117 fget=_getter, 

118 fset=_setter, 

119 doc=getattr(delegate, accessor_mapping(name)).__doc__, 

120 ) 

121 

122 def _create_delegator_method(name: str): 

123 method = getattr(delegate, accessor_mapping(name)) 

124 

125 @functools.wraps(method) 

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

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

128 

129 return f 

130 

131 for name in accessors: 

132 if ( 

133 not raise_on_missing 

134 and getattr(delegate, accessor_mapping(name), None) is None 

135 ): 

136 continue 

137 

138 if typ == "property": 

139 f = _create_delegator_property(name) 

140 else: 

141 f = _create_delegator_method(name) 

142 

143 # don't overwrite existing methods/properties 

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

145 setattr(cls, name, f) 

146 

147 

148def delegate_names( 

149 delegate, 

150 accessors: list[str], 

151 typ: str, 

152 overwrite: bool = False, 

153 accessor_mapping: Callable[[str], str] = lambda x: x, 

154 raise_on_missing: bool = True, 

155): 

156 """ 

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

158 an alternative usage to directly calling `_add_delegate_accessors` 

159 below a class definition. 

160 

161 Parameters 

162 ---------- 

163 delegate : object 

164 The class to get methods/properties & docstrings. 

165 accessors : Sequence[str] 

166 List of accessor to add. 

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

168 overwrite : bool, default False 

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

170 accessor_mapping: Callable, default lambda x: x 

171 Callable to map the delegate's function to the cls' function. 

172 raise_on_missing: bool, default True 

173 Raise if an accessor does not exist on delegate. 

174 False skips the missing accessor. 

175 

176 Returns 

177 ------- 

178 callable 

179 A class decorator. 

180 

181 Examples 

182 -------- 

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

184 class CategoricalAccessor(PandasDelegate): 

185 [...] 

186 """ 

187 

188 def add_delegate_accessors(cls): 

189 cls._add_delegate_accessors( 

190 delegate, 

191 accessors, 

192 typ, 

193 overwrite=overwrite, 

194 accessor_mapping=accessor_mapping, 

195 raise_on_missing=raise_on_missing, 

196 ) 

197 return cls 

198 

199 return add_delegate_accessors 

200 

201 

202class Accessor: 

203 """ 

204 Custom property-like object. 

205 

206 A descriptor for accessors. 

207 

208 Parameters 

209 ---------- 

210 name : str 

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

212 accessor : cls 

213 Class with the extension methods. 

214 

215 Notes 

216 ----- 

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

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

219 single argument ``data``. 

220 """ 

221 

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

223 self._name = name 

224 self._accessor = accessor 

225 

226 def __get__(self, obj, cls): 

227 if obj is None: 

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

229 return self._accessor 

230 return self._accessor(obj) 

231 

232 

233# Alias kept for downstream libraries 

234# TODO: Deprecate as name is now misleading 

235CachedAccessor = Accessor 

236 

237 

238def _register_accessor( 

239 name: str, cls: type[NDFrame | Index] 

240) -> Callable[[TypeT], TypeT]: 

241 """ 

242 Register a custom accessor on objects. 

243 

244 Parameters 

245 ---------- 

246 name : str 

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

248 if this name conflicts with a preexisting attribute. 

249 

250 Returns 

251 ------- 

252 callable 

253 A class decorator. 

254 

255 See Also 

256 -------- 

257 register_dataframe_accessor : Register a custom accessor on DataFrame objects. 

258 register_series_accessor : Register a custom accessor on Series objects. 

259 register_index_accessor : Register a custom accessor on Index objects. 

260 

261 Notes 

262 ----- 

263 This function allows you to register a custom-defined accessor class 

264 for pandas objects (DataFrame, Series, or Index). 

265 The requirements for the accessor class are as follows: 

266 

267 * Must contain an init method that: 

268 

269 * accepts a single object 

270 

271 * raises an AttributeError if the object does not have correctly 

272 matching inputs for the accessor 

273 

274 * Must contain a method for each access pattern. 

275 

276 * The methods should be able to take any argument signature. 

277 

278 * Accessible using the @property decorator if no additional arguments are 

279 needed. 

280 

281 """ 

282 

283 def decorator(accessor: TypeT) -> TypeT: 

284 if hasattr(cls, name): 

285 warnings.warn( 

286 f"registration of accessor {accessor!r} under name " 

287 f"{name!r} for type {cls!r} is overriding a preexisting " 

288 f"attribute with the same name.", 

289 UserWarning, 

290 stacklevel=find_stack_level(), 

291 ) 

292 setattr(cls, name, Accessor(name, accessor)) 

293 cls._accessors.add(name) 

294 return accessor 

295 

296 return decorator 

297 

298 

299_register_df_examples = """ 

300An accessor that only accepts integers could 

301have a class defined like this: 

302 

303>>> @pd.api.extensions.register_dataframe_accessor("int_accessor") 

304... class IntAccessor: 

305... def __init__(self, pandas_obj): 

306... if not all(pandas_obj[col].dtype == 'int64' for col in pandas_obj.columns): 

307... raise AttributeError("All columns must contain integer values only") 

308... self._obj = pandas_obj 

309... 

310... def sum(self): 

311... return self._obj.sum() 

312... 

313>>> df = pd.DataFrame([[1, 2], ['x', 'y']]) 

314>>> df.int_accessor 

315Traceback (most recent call last): 

316... 

317AttributeError: All columns must contain integer values only. 

318>>> df = pd.DataFrame([[1, 2], [3, 4]]) 

319>>> df.int_accessor.sum() 

3200 4 

3211 6 

322dtype: int64""" 

323 

324 

325@set_module("pandas.api.extensions") 

326def register_dataframe_accessor(name: str) -> Callable[[TypeT], TypeT]: 

327 """ 

328 Register a custom accessor on DataFrame objects. 

329 

330 Parameters 

331 ---------- 

332 name : str 

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

334 if this name conflicts with a preexisting attribute. 

335 

336 Returns 

337 ------- 

338 callable 

339 A class decorator. 

340 

341 See Also 

342 -------- 

343 register_dataframe_accessor : Register a custom accessor on DataFrame objects. 

344 register_series_accessor : Register a custom accessor on Series objects. 

345 register_index_accessor : Register a custom accessor on Index objects. 

346 

347 Notes 

348 ----- 

349 This function allows you to register a custom-defined accessor class for DataFrame. 

350 The requirements for the accessor class are as follows: 

351 

352 * Must contain an init method that: 

353 

354 * accepts a single DataFrame object 

355 

356 * raises an AttributeError if the DataFrame object does not have correctly 

357 matching inputs for the accessor 

358 

359 * Must contain a method for each access pattern. 

360 

361 * The methods should be able to take any argument signature. 

362 

363 * Accessible using the @property decorator if no additional arguments are 

364 needed. 

365 

366 Examples 

367 -------- 

368 An accessor that only accepts integers could 

369 have a class defined like this: 

370 

371 >>> @pd.api.extensions.register_dataframe_accessor("int_accessor") 

372 ... class IntAccessor: 

373 ... def __init__(self, pandas_obj): 

374 ... if not all( 

375 ... pandas_obj[col].dtype == "int64" for col in pandas_obj.columns 

376 ... ): 

377 ... raise AttributeError("All columns must contain integer values only") 

378 ... self._obj = pandas_obj 

379 ... 

380 ... def sum(self): 

381 ... return self._obj.sum() 

382 >>> df = pd.DataFrame([[1, 2], ["x", "y"]]) 

383 >>> df.int_accessor 

384 Traceback (most recent call last): 

385 ... 

386 AttributeError: All columns must contain integer values only. 

387 >>> df = pd.DataFrame([[1, 2], [3, 4]]) 

388 >>> df.int_accessor.sum() 

389 0 4 

390 1 6 

391 dtype: int64 

392 """ 

393 from pandas import DataFrame 

394 

395 return _register_accessor(name, DataFrame) 

396 

397 

398_register_series_examples = """ 

399An accessor that only accepts integers could 

400have a class defined like this: 

401 

402>>> @pd.api.extensions.register_series_accessor("int_accessor") 

403... class IntAccessor: 

404... def __init__(self, pandas_obj): 

405... if not pandas_obj.dtype == 'int64': 

406... raise AttributeError("The series must contain integer data only") 

407... self._obj = pandas_obj 

408... 

409... def sum(self): 

410... return self._obj.sum() 

411... 

412>>> df = pd.Series([1, 2, 'x']) 

413>>> df.int_accessor 

414Traceback (most recent call last): 

415... 

416AttributeError: The series must contain integer data only. 

417>>> df = pd.Series([1, 2, 3]) 

418>>> df.int_accessor.sum() 

4196""" 

420 

421 

422@set_module("pandas.api.extensions") 

423def register_series_accessor(name: str) -> Callable[[TypeT], TypeT]: 

424 """ 

425 Register a custom accessor on Series objects. 

426 

427 Parameters 

428 ---------- 

429 name : str 

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

431 if this name conflicts with a preexisting attribute. 

432 

433 Returns 

434 ------- 

435 callable 

436 A class decorator. 

437 

438 See Also 

439 -------- 

440 register_dataframe_accessor : Register a custom accessor on DataFrame objects. 

441 register_series_accessor : Register a custom accessor on Series objects. 

442 register_index_accessor : Register a custom accessor on Index objects. 

443 

444 Notes 

445 ----- 

446 This function allows you to register a custom-defined accessor class for Series. 

447 The requirements for the accessor class are as follows: 

448 

449 * Must contain an init method that: 

450 

451 * accepts a single Series object 

452 

453 * raises an AttributeError if the Series object does not have correctly 

454 matching inputs for the accessor 

455 

456 * Must contain a method for each access pattern. 

457 

458 * The methods should be able to take any argument signature. 

459 

460 * Accessible using the @property decorator if no additional arguments are 

461 needed. 

462 

463 Examples 

464 -------- 

465 An accessor that only accepts integers could 

466 have a class defined like this: 

467 

468 >>> @pd.api.extensions.register_series_accessor("int_accessor") 

469 ... class IntAccessor: 

470 ... def __init__(self, pandas_obj): 

471 ... if not pandas_obj.dtype == "int64": 

472 ... raise AttributeError("The series must contain integer data only") 

473 ... self._obj = pandas_obj 

474 ... 

475 ... def sum(self): 

476 ... return self._obj.sum() 

477 >>> df = pd.Series([1, 2, "x"]) 

478 >>> df.int_accessor 

479 Traceback (most recent call last): 

480 ... 

481 AttributeError: The series must contain integer data only. 

482 >>> df = pd.Series([1, 2, 3]) 

483 >>> df.int_accessor.sum() 

484 6 

485 """ 

486 from pandas import Series 

487 

488 return _register_accessor(name, Series) 

489 

490 

491_register_index_examples = """ 

492An accessor that only accepts integers could 

493have a class defined like this: 

494 

495>>> @pd.api.extensions.register_index_accessor("int_accessor") 

496... class IntAccessor: 

497... def __init__(self, pandas_obj): 

498... if not all(isinstance(x, int) for x in pandas_obj): 

499... raise AttributeError("The index must only be an integer value") 

500... self._obj = pandas_obj 

501... 

502... def even(self): 

503... return [x for x in self._obj if x % 2 == 0] 

504>>> df = pd.DataFrame.from_dict( 

505... {"row1": {"1": 1, "2": "a"}, "row2": {"1": 2, "2": "b"}}, orient="index" 

506... ) 

507>>> df.index.int_accessor 

508Traceback (most recent call last): 

509... 

510AttributeError: The index must only be an integer value. 

511>>> df = pd.DataFrame( 

512... {"col1": [1, 2, 3, 4], "col2": ["a", "b", "c", "d"]}, index=[1, 2, 5, 8] 

513... ) 

514>>> df.index.int_accessor.even() 

515[2, 8]""" 

516 

517 

518@set_module("pandas.api.extensions") 

519def register_index_accessor(name: str) -> Callable[[TypeT], TypeT]: 

520 """ 

521 Register a custom accessor on Index objects. 

522 

523 Parameters 

524 ---------- 

525 name : str 

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

527 if this name conflicts with a preexisting attribute. 

528 

529 Returns 

530 ------- 

531 callable 

532 A class decorator. 

533 

534 See Also 

535 -------- 

536 register_dataframe_accessor : Register a custom accessor on DataFrame objects. 

537 register_series_accessor : Register a custom accessor on Series objects. 

538 register_index_accessor : Register a custom accessor on Index objects. 

539 

540 Notes 

541 ----- 

542 This function allows you to register a custom-defined accessor class for Index. 

543 The requirements for the accessor class are as follows: 

544 

545 * Must contain an init method that: 

546 

547 * accepts a single Index object 

548 

549 * raises an AttributeError if the Index object does not have correctly 

550 matching inputs for the accessor 

551 

552 * Must contain a method for each access pattern. 

553 

554 * The methods should be able to take any argument signature. 

555 

556 * Accessible using the @property decorator if no additional arguments are 

557 needed. 

558 

559 Examples 

560 -------- 

561 An accessor that only accepts integers could 

562 have a class defined like this: 

563 

564 >>> @pd.api.extensions.register_index_accessor("int_accessor") 

565 ... class IntAccessor: 

566 ... def __init__(self, pandas_obj): 

567 ... if not all(isinstance(x, int) for x in pandas_obj): 

568 ... raise AttributeError("The index must only be an integer value") 

569 ... self._obj = pandas_obj 

570 ... 

571 ... def even(self): 

572 ... return [x for x in self._obj if x % 2 == 0] 

573 >>> df = pd.DataFrame.from_dict( 

574 ... {"row1": {"1": 1, "2": "a"}, "row2": {"1": 2, "2": "b"}}, orient="index" 

575 ... ) 

576 >>> df.index.int_accessor 

577 Traceback (most recent call last): 

578 ... 

579 AttributeError: The index must only be an integer value. 

580 >>> df = pd.DataFrame( 

581 ... {"col1": [1, 2, 3, 4], "col2": ["a", "b", "c", "d"]}, index=[1, 2, 5, 8] 

582 ... ) 

583 >>> df.index.int_accessor.even() 

584 [2, 8] 

585 """ 

586 from pandas import Index 

587 

588 return _register_accessor(name, Index)