Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/core/dtypes/base.py: 56%

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

157 statements  

1""" 

2Extend pandas with custom array types. 

3""" 

4 

5from __future__ import annotations 

6 

7from typing import ( 

8 TYPE_CHECKING, 

9 Any, 

10 Self, 

11 TypeVar, 

12 cast, 

13 overload, 

14) 

15 

16import numpy as np 

17 

18from pandas._libs import missing as libmissing 

19from pandas._libs.hashtable import object_hash 

20from pandas._libs.properties import cache_readonly 

21from pandas.errors import AbstractMethodError 

22from pandas.util._decorators import set_module 

23 

24from pandas.core.dtypes.generic import ( 

25 ABCDataFrame, 

26 ABCIndex, 

27 ABCSeries, 

28) 

29 

30if TYPE_CHECKING: 

31 from pandas._typing import ( 

32 DtypeObj, 

33 Shape, 

34 npt, 

35 type_t, 

36 ) 

37 

38 from pandas import Index 

39 from pandas.core.arrays import ExtensionArray 

40 

41 # To parameterize on same ExtensionDtype 

42 ExtensionDtypeT = TypeVar("ExtensionDtypeT", bound="ExtensionDtype") 

43 

44 

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

46class ExtensionDtype: 

47 """ 

48 A custom data type, to be paired with an ExtensionArray. 

49 

50 This enables support for third-party and custom dtypes within the 

51 pandas ecosystem. By implementing this interface and pairing it with a custom 

52 `ExtensionArray`, users can create rich data types that integrate cleanly 

53 with pandas operations, such as grouping, joining, or aggregation. 

54 

55 See Also 

56 -------- 

57 extensions.register_extension_dtype: Register an ExtensionType 

58 with pandas as class decorator. 

59 extensions.ExtensionArray: Abstract base class for custom 1-D array types. 

60 

61 Notes 

62 ----- 

63 The interface includes the following abstract methods that must 

64 be implemented by subclasses: 

65 

66 * type 

67 * name 

68 * construct_array_type 

69 

70 The following attributes and methods influence the behavior of the dtype in 

71 pandas operations 

72 

73 * _is_numeric 

74 * _is_boolean 

75 * _get_common_dtype 

76 

77 The `na_value` class attribute can be used to set the default NA value 

78 for this type. :attr:`numpy.nan` is used by default. 

79 

80 ExtensionDtypes are required to be hashable. The base class provides 

81 a default implementation, which relies on the ``_metadata`` class 

82 attribute. ``_metadata`` should be a tuple containing the strings 

83 that define your data type. For example, with ``PeriodDtype`` that's 

84 the ``freq`` attribute. 

85 

86 **If you have a parametrized dtype you should set the ``_metadata`` 

87 class property**. 

88 

89 Ideally, the attributes in ``_metadata`` will match the 

90 parameters to your ``ExtensionDtype.__init__`` (if any). If any of 

91 the attributes in ``_metadata`` don't implement the standard 

92 ``__eq__`` or ``__hash__``, the default implementations here will not 

93 work. 

94 

95 Examples 

96 -------- 

97 

98 For interaction with Apache Arrow (pyarrow), a ``__from_arrow__`` method 

99 can be implemented: this method receives a pyarrow Array or ChunkedArray 

100 as only argument and is expected to return the appropriate pandas 

101 ExtensionArray for this dtype and the passed values: 

102 

103 >>> import pyarrow 

104 >>> from pandas.api.extensions import ExtensionArray 

105 >>> class ExtensionDtype: 

106 ... def __from_arrow__( 

107 ... self, array: pyarrow.Array | pyarrow.ChunkedArray 

108 ... ) -> ExtensionArray: ... 

109 

110 This class does not inherit from 'abc.ABCMeta' for performance reasons. 

111 Methods and properties required by the interface raise 

112 ``pandas.errors.AbstractMethodError`` and no ``register`` method is 

113 provided for registering virtual subclasses. 

114 """ 

115 

116 _metadata: tuple[str, ...] = () 

117 

118 def __str__(self) -> str: 

119 return self.name 

120 

121 def __eq__(self, other: object) -> bool: 

122 """ 

123 Check whether 'other' is equal to self. 

124 

125 By default, 'other' is considered equal if either 

126 

127 * it's a string matching 'self.name'. 

128 * it's an instance of this type and all of the attributes 

129 in ``self._metadata`` are equal between `self` and `other`. 

130 

131 Parameters 

132 ---------- 

133 other : Any 

134 

135 Returns 

136 ------- 

137 bool 

138 """ 

139 if isinstance(other, str): 

140 try: 

141 other = self.construct_from_string(other) 

142 except TypeError: 

143 return False 

144 if isinstance(other, type(self)): 

145 return all( 

146 getattr(self, attr) == getattr(other, attr) for attr in self._metadata 

147 ) 

148 return False 

149 

150 def __hash__(self) -> int: 

151 # different nan objects have different hashes 

152 # we need to avoid that and thus use hash function with old behavior 

153 return object_hash(tuple(getattr(self, attr) for attr in self._metadata)) 

154 

155 def __ne__(self, other: object) -> bool: 

156 return not self.__eq__(other) 

157 

158 @property 

159 def na_value(self) -> object: 

160 """ 

161 Default NA value to use for this type. 

162 

163 This is used in e.g. ExtensionArray.take. This should be the 

164 user-facing "boxed" version of the NA value, not the physical NA value 

165 for storage. e.g. for JSONArray, this is an empty dictionary. 

166 """ 

167 return np.nan 

168 

169 @property 

170 def type(self) -> type_t[Any]: 

171 """ 

172 The scalar type for the array, e.g. ``int`` 

173 

174 It's expected ``ExtensionArray[item]`` returns an instance 

175 of ``ExtensionDtype.type`` for scalar ``item``, assuming 

176 that value is valid (not NA). NA values do not need to be 

177 instances of `type`. 

178 """ 

179 raise AbstractMethodError(self) 

180 

181 @property 

182 def kind(self) -> str: 

183 """ 

184 A character code (one of 'biufcmMOSUV'), default 'O' 

185 

186 This should match the NumPy dtype used when the array is 

187 converted to an ndarray, which is probably 'O' for object if 

188 the extension type cannot be represented as a built-in NumPy 

189 type. 

190 

191 See Also 

192 -------- 

193 numpy.dtype.kind 

194 """ 

195 return "O" 

196 

197 @property 

198 def name(self) -> str: 

199 """ 

200 A string identifying the data type. 

201 

202 Will be used for display in, e.g. ``Series.dtype`` 

203 """ 

204 raise AbstractMethodError(self) 

205 

206 @property 

207 def names(self) -> list[str] | None: 

208 """ 

209 Ordered list of field names, or None if there are no fields. 

210 

211 This is for compatibility with NumPy arrays, and may be removed in the 

212 future. 

213 """ 

214 return None 

215 

216 def construct_array_type(self) -> type_t[ExtensionArray]: 

217 """ 

218 Return the array type associated with this dtype. 

219 

220 Returns 

221 ------- 

222 type 

223 """ 

224 raise AbstractMethodError(self) 

225 

226 def empty(self, shape: Shape) -> ExtensionArray: 

227 """ 

228 Construct an ExtensionArray of this dtype with the given shape. 

229 

230 Analogous to numpy.empty. 

231 

232 Parameters 

233 ---------- 

234 shape : int or tuple[int] 

235 

236 Returns 

237 ------- 

238 ExtensionArray 

239 """ 

240 cls = self.construct_array_type() 

241 return cls._empty(shape, dtype=self) 

242 

243 @classmethod 

244 def construct_from_string(cls, string: str) -> Self: 

245 r""" 

246 Construct this type from a string. 

247 

248 This is useful mainly for data types that accept parameters. 

249 For example, a period dtype accepts a frequency parameter that 

250 can be set as ``period[h]`` (where H means hourly frequency). 

251 

252 By default, in the abstract class, just the name of the type is 

253 expected. But subclasses can overwrite this method to accept 

254 parameters. 

255 

256 Parameters 

257 ---------- 

258 string : str 

259 The name of the type, for example ``category``. 

260 

261 Returns 

262 ------- 

263 ExtensionDtype 

264 Instance of the dtype. 

265 

266 Raises 

267 ------ 

268 TypeError 

269 If a class cannot be constructed from this 'string'. 

270 

271 Examples 

272 -------- 

273 For extension dtypes with arguments the following may be an 

274 adequate implementation. 

275 

276 >>> import re 

277 >>> @classmethod 

278 ... def construct_from_string(cls, string): 

279 ... pattern = re.compile(r"^my_type\[(?P<arg_name>.+)\]$") 

280 ... match = pattern.match(string) 

281 ... if match: 

282 ... return cls(**match.groupdict()) 

283 ... else: 

284 ... raise TypeError( 

285 ... f"Cannot construct a '{cls.__name__}' from '{string}'" 

286 ... ) 

287 """ 

288 if not isinstance(string, str): 

289 raise TypeError( 

290 f"'construct_from_string' expects a string, got {type(string)}" 

291 ) 

292 # error: Non-overlapping equality check (left operand type: "str", right 

293 # operand type: "Callable[[ExtensionDtype], str]") [comparison-overlap] 

294 assert isinstance(cls.name, str), (cls, type(cls.name)) 

295 if string != cls.name: 

296 raise TypeError(f"Cannot construct a '{cls.__name__}' from '{string}'") 

297 return cls() 

298 

299 @classmethod 

300 def is_dtype(cls, dtype: object) -> bool: 

301 """ 

302 Check if we match 'dtype'. 

303 

304 Parameters 

305 ---------- 

306 dtype : object 

307 The object to check. 

308 

309 Returns 

310 ------- 

311 bool 

312 

313 Notes 

314 ----- 

315 The default implementation is True if 

316 

317 1. ``cls.construct_from_string(dtype)`` is an instance 

318 of ``cls``. 

319 2. ``dtype`` is an object and is an instance of ``cls`` 

320 3. ``dtype`` has a ``dtype`` attribute, and any of the above 

321 conditions is true for ``dtype.dtype``. 

322 """ 

323 dtype = getattr(dtype, "dtype", dtype) 

324 

325 if isinstance(dtype, (ABCSeries, ABCIndex, ABCDataFrame, np.dtype)): 

326 # https://github.com/pandas-dev/pandas/issues/22960 

327 # avoid passing data to `construct_from_string`. This could 

328 # cause a FutureWarning from numpy about failing elementwise 

329 # comparison from, e.g., comparing DataFrame == 'category'. 

330 return False 

331 elif dtype is None: 

332 return False 

333 elif isinstance(dtype, cls): 

334 return True 

335 if isinstance(dtype, str): 

336 try: 

337 return cls.construct_from_string(dtype) is not None 

338 except TypeError: 

339 return False 

340 return False 

341 

342 @property 

343 def _is_numeric(self) -> bool: 

344 """ 

345 Whether columns with this dtype should be considered numeric. 

346 

347 By default ExtensionDtypes are assumed to be non-numeric. 

348 They'll be excluded from operations that exclude non-numeric 

349 columns, like (groupby) reductions, plotting, etc. 

350 """ 

351 return False 

352 

353 @property 

354 def _is_boolean(self) -> bool: 

355 """ 

356 Whether this dtype should be considered boolean. 

357 

358 By default, ExtensionDtypes are assumed to be non-numeric. 

359 Setting this to True will affect the behavior of several places, 

360 e.g. 

361 

362 * is_bool 

363 * boolean indexing 

364 

365 Returns 

366 ------- 

367 bool 

368 """ 

369 return False 

370 

371 def _get_common_dtype(self, dtypes: list[DtypeObj]) -> DtypeObj | None: 

372 """ 

373 Return the common dtype, if one exists. 

374 

375 Used in `find_common_type` implementation. This is for example used 

376 to determine the resulting dtype in a concat operation. 

377 

378 If no common dtype exists, return None (which gives the other dtypes 

379 the chance to determine a common dtype). If all dtypes in the list 

380 return None, then the common dtype will be "object" dtype (this means 

381 it is never needed to return "object" dtype from this method itself). 

382 

383 Parameters 

384 ---------- 

385 dtypes : list of dtypes 

386 The dtypes for which to determine a common dtype. This is a list 

387 of np.dtype or ExtensionDtype instances. 

388 

389 Returns 

390 ------- 

391 Common dtype (np.dtype or ExtensionDtype) or None 

392 """ 

393 if len(set(dtypes)) == 1: 

394 # only itself 

395 return self 

396 else: 

397 return None 

398 

399 @property 

400 def _can_hold_na(self) -> bool: 

401 """ 

402 Can arrays of this dtype hold NA values? 

403 """ 

404 return True 

405 

406 @property 

407 def _is_immutable(self) -> bool: 

408 """ 

409 Can arrays with this dtype be modified with __setitem__? If not, return 

410 True. 

411 

412 Immutable arrays are expected to raise TypeError on __setitem__ calls. 

413 """ 

414 return False 

415 

416 @cache_readonly 

417 def index_class(self) -> type_t[Index]: 

418 """ 

419 The Index subclass to return from Index.__new__ when this dtype is 

420 encountered. 

421 """ 

422 from pandas import Index 

423 

424 return Index 

425 

426 @property 

427 def _supports_2d(self) -> bool: 

428 """ 

429 Do ExtensionArrays with this dtype support 2D arrays? 

430 

431 Historically ExtensionArrays were limited to 1D. By returning True here, 

432 authors can indicate that their arrays support 2D instances. This can 

433 improve performance in some cases, particularly operations with `axis=1`. 

434 

435 Arrays that support 2D values should: 

436 

437 - implement Array.reshape 

438 - subclass the Dim2CompatTests in tests.extension.base 

439 - _concat_same_type should support `axis` keyword 

440 - _reduce and reductions should support `axis` keyword 

441 """ 

442 return False 

443 

444 @property 

445 def _can_fast_transpose(self) -> bool: 

446 """ 

447 Is transposing an array with this dtype zero-copy? 

448 

449 Only relevant for cases where _supports_2d is True. 

450 """ 

451 return False 

452 

453 

454class StorageExtensionDtype(ExtensionDtype): 

455 """ExtensionDtype that may be backed by more than one implementation.""" 

456 

457 name: str 

458 _metadata = ("storage",) 

459 

460 def __init__(self, storage: str) -> None: 

461 self._storage = storage 

462 

463 def __repr__(self) -> str: 

464 return f"{self.name}[{self.storage}]" 

465 

466 def __str__(self) -> str: 

467 return self.name 

468 

469 def __eq__(self, other: object) -> bool: 

470 if isinstance(other, str) and other == self.name: 

471 return True 

472 return super().__eq__(other) 

473 

474 def __hash__(self) -> int: 

475 # custom __eq__ so have to override __hash__ 

476 return super().__hash__() 

477 

478 @property 

479 def na_value(self) -> libmissing.NAType: 

480 return libmissing.NA 

481 

482 @property 

483 def storage(self) -> str: 

484 return self._storage 

485 

486 

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

488def register_extension_dtype(cls: type_t[ExtensionDtypeT]) -> type_t[ExtensionDtypeT]: 

489 """ 

490 Register an ExtensionType with pandas as class decorator. 

491 

492 This enables operations like ``.astype(name)`` for the name 

493 of the ExtensionDtype. 

494 

495 Returns 

496 ------- 

497 callable 

498 A class decorator. 

499 

500 See Also 

501 -------- 

502 api.extensions.ExtensionDtype : The base class for creating custom pandas 

503 data types. 

504 Series : One-dimensional array with axis labels. 

505 DataFrame : Two-dimensional, size-mutable, potentially heterogeneous 

506 tabular data. 

507 

508 Examples 

509 -------- 

510 >>> from pandas.api.extensions import register_extension_dtype, ExtensionDtype 

511 >>> @register_extension_dtype 

512 ... class MyExtensionDtype(ExtensionDtype): 

513 ... name = "myextension" 

514 """ 

515 _registry.register(cls) 

516 return cls 

517 

518 

519class Registry: 

520 """ 

521 Registry for dtype inference. 

522 

523 The registry allows one to map a string repr of an extension 

524 dtype to an extension dtype. The string alias can be used in several 

525 places, including 

526 

527 * Series and Index constructors 

528 * :meth:`pandas.array` 

529 * :meth:`pandas.Series.astype` 

530 

531 Multiple extension types can be registered. 

532 These are tried in order. 

533 """ 

534 

535 def __init__(self) -> None: 

536 self.dtypes: list[type_t[ExtensionDtype]] = [] 

537 

538 def register(self, dtype: type_t[ExtensionDtype]) -> None: 

539 """ 

540 Parameters 

541 ---------- 

542 dtype : ExtensionDtype class 

543 """ 

544 if not issubclass(dtype, ExtensionDtype): 

545 raise ValueError("can only register pandas extension dtypes") 

546 

547 self.dtypes.append(dtype) 

548 

549 @overload 

550 def find(self, dtype: type_t[ExtensionDtypeT]) -> type_t[ExtensionDtypeT]: ... 

551 

552 @overload 

553 def find(self, dtype: ExtensionDtypeT) -> ExtensionDtypeT: ... 

554 

555 @overload 

556 def find(self, dtype: str) -> ExtensionDtype | None: ... 

557 

558 @overload 

559 def find( 

560 self, dtype: npt.DTypeLike 

561 ) -> type_t[ExtensionDtype] | ExtensionDtype | None: ... 

562 

563 def find( 

564 self, dtype: type_t[ExtensionDtype] | ExtensionDtype | npt.DTypeLike 

565 ) -> type_t[ExtensionDtype] | ExtensionDtype | None: 

566 """ 

567 Parameters 

568 ---------- 

569 dtype : ExtensionDtype class or instance or str or numpy dtype or python type 

570 

571 Returns 

572 ------- 

573 return the first matching dtype, otherwise return None 

574 """ 

575 if not isinstance(dtype, str): 

576 dtype_type: type_t 

577 if not isinstance(dtype, type): 

578 dtype_type = type(dtype) 

579 else: 

580 dtype_type = dtype 

581 if issubclass(dtype_type, ExtensionDtype): 

582 # cast needed here as mypy doesn't know we have figured 

583 # out it is an ExtensionDtype or type_t[ExtensionDtype] 

584 return cast("ExtensionDtype | type_t[ExtensionDtype]", dtype) 

585 

586 return None 

587 

588 for dtype_type in self.dtypes: 

589 try: 

590 return dtype_type.construct_from_string(dtype) 

591 except TypeError: 

592 pass 

593 

594 return None 

595 

596 

597_registry = Registry()