├── .gitignore ├── LICENSE.txt ├── README.md ├── pep-0661.rst ├── sentinels └── sentinels.py └── test └── test_sentinels.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # Unit test / coverage reports 31 | htmlcov/ 32 | .tox/ 33 | .nox/ 34 | .coverage 35 | .coverage.* 36 | .cache 37 | nosetests.xml 38 | coverage.xml 39 | *.cover 40 | *.py,cover 41 | .hypothesis/ 42 | .pytest_cache/ 43 | 44 | # pyenv 45 | .python-version 46 | 47 | # Environments 48 | .env 49 | .venv 50 | env/ 51 | venv/ 52 | ENV/ 53 | env.bak/ 54 | venv.bak/ 55 | 56 | # PyCharm 57 | .idea/ 58 | 59 | # Mac 60 | .DS_Store 61 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2021-2022 Tal Einat 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the “Software”), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sentinels 2 | 3 | This is a reference implementation of a utility for the definition of 4 | sentinel values in Python. This also includes a [draft PEP](pep-0661.rst) for 5 | the inclusion of this utility in the Python standard library. 6 | 7 | ## Usage 8 | 9 | ```python 10 | from sentinels import Sentinel 11 | 12 | NotGiven = Sentinel('NotGiven') 13 | 14 | MEGA = Sentinel('MEGA', repr='', module_name='my_mod') 15 | ``` 16 | 17 | ## References 18 | 19 | * [Discussion on the python-dev mailing list](https://mail.python.org/archives/list/python-dev@python.org/thread/ZLVPD2OISI7M4POMTR2FCQTE6TPMPTO3/) 20 | * [Poll and additional discussion on discuss.python.org](https://discuss.python.org/t/sentinel-values-in-the-stdlib/8810) 21 | -------------------------------------------------------------------------------- /pep-0661.rst: -------------------------------------------------------------------------------- 1 | PEP: 661 2 | Title: Sentinel Values 3 | Author: Tal Einat 4 | Discussions-To: https://discuss.python.org/t/pep-661-sentinel-values/9126 5 | Status: Draft 6 | Type: Standards Track 7 | Content-Type: text/x-rst 8 | Created: 06-Jun-2021 9 | Post-History: `20-May-2021 `__, `06-Jun-2021 `__ 10 | 11 | 12 | TL;DR: See the `Specification`_ and `Reference Implementation`_. 13 | 14 | 15 | Abstract 16 | ======== 17 | 18 | Unique placeholder values, commonly known as "sentinel values", are common in 19 | programming. They have many uses, such as for: 20 | 21 | * Default values for function arguments, for when a value was not given:: 22 | 23 | def foo(value=None): 24 | ... 25 | 26 | * Return values from functions when something is not found or unavailable:: 27 | 28 | >>> "abc".find("d") 29 | -1 30 | 31 | * Missing data, such as NULL in relational databases or "N/A" ("not 32 | available") in spreadsheets 33 | 34 | Python has the special value ``None``, which is intended to be used as such 35 | a sentinel value in most cases. However, sometimes an alternative sentinel 36 | value is needed, usually when it needs to be distinct from ``None`` since 37 | ``None`` is a valid value in that context. Such cases are common enough that 38 | several idioms for implementing such sentinels have arisen over the years, but 39 | uncommon enough that there hasn't been a clear need for standardization. 40 | However, the common implementations, including some in the stdlib, suffer from 41 | several significant drawbacks. 42 | 43 | This PEP proposes adding a utility for defining sentinel values, to be used 44 | in the stdlib and made publicly available as part of the stdlib. 45 | 46 | Note: Changing all existing sentinels in the stdlib to be implemented this 47 | way is not deemed necessary, and whether to do so is left to the discretion 48 | of the maintainers. 49 | 50 | 51 | Motivation 52 | ========== 53 | 54 | In May 2021, a question was brought up on the python-dev mailing list 55 | [1]_ about how to better implement a sentinel value for 56 | ``traceback.print_exception``. The existing implementation used the 57 | following common idiom:: 58 | 59 | _sentinel = object() 60 | 61 | However, this object has an uninformative and overly verbose repr, causing the 62 | function's signature to be overly long and hard to read:: 63 | 64 | >>> help(traceback.print_exception) 65 | Help on function print_exception in module traceback: 66 | 67 | print_exception(exc, /, value=, tb=, 69 | limit=None, file=None, chain=True) 70 | 71 | Additionally, two other drawbacks of many existing sentinels were brought up 72 | in the discussion: 73 | 74 | 1. Some do not have a distinct type, hence it is impossible to define clear 75 | type signatures for functions with such sentinels as default values. 76 | 2. They behave unexpectedly after being copied or unpickled, due to a separate 77 | instance being created and thus comparisons using ``is`` failing. 78 | 79 | In the ensuing discussion, Victor Stinner supplied a list of currently used 80 | sentinel values in the Python standard library [2]_. This showed that the 81 | need for sentinels is fairly common, that there are various implementation 82 | methods used even within the stdlib, and that many of these suffer from at 83 | least one of the three above drawbacks. 84 | 85 | The discussion did not lead to any clear consensus on whether a standard 86 | implementation method is needed or desirable, whether the drawbacks mentioned 87 | are significant, nor which kind of implementation would be good. The author 88 | of this PEP created an issue on bugs.python.org (now a GitHub issue [3]_) 89 | suggesting options for improvement, but that focused on only a single 90 | problematic aspect of a few cases, and failed to gather any support. 91 | 92 | A poll [4]_ was created on discuss.python.org to get a clearer sense of 93 | the community's opinions. After nearly two weeks, significant further, 94 | discussion, and 39 votes, the poll's results were not conclusive. 40% had 95 | voted for "The status-quo is fine / there’s no need for consistency in 96 | this", but most voters had voted for one or more standardized solutions. 97 | Specifically, 37% of the voters chose "Consistent use of a new, dedicated 98 | sentinel factory / class / meta-class, also made publicly available in the 99 | stdlib". 100 | 101 | With such mixed opinions, this PEP was created to facilitate making a decision 102 | on the subject. 103 | 104 | While working on this PEP, iterating on various options and implementations 105 | and continuing discussions, the author has come to the opinion that a simple, 106 | good implementation available in the standard library would be worth having, 107 | both for use in the standard library itself and elsewhere. 108 | 109 | 110 | Rationale 111 | ========= 112 | 113 | The criteria guiding the chosen implementation were: 114 | 115 | 1. The sentinel objects should behave as expected by a sentinel object: When 116 | compared using the ``is`` operator, it should always be considered 117 | identical to itself but never to any other object. 118 | 2. Creating a sentinel object should be a simple, straightforward one-liner. 119 | 3. It should be simple to define as many distinct sentinel values as needed. 120 | 4. The sentinel objects should have a clear and short repr. 121 | 5. It should be possible to use clear type signatures for sentinels. 122 | 6. The sentinel objects should behave correctly after copying and/or 123 | unpickling. 124 | 7. Such sentinels should work when using CPython 3.x and PyPy3, and ideally 125 | also with other implementations of Python. 126 | 8. As simple and straightforward as possible, in implementation and especially 127 | in use. Avoid this becoming one more special thing to learn when learning 128 | Python. It should be easy to find and use when needed, and obvious enough 129 | when reading code that one would normally not feel a need to look up its 130 | documentation. 131 | 132 | With so many uses in the Python standard library [2]_, it would be useful to 133 | have an implementation in the standard library, since the stdlib cannot use 134 | implementations of sentinel objects available elsewhere (such as the 135 | ``sentinels`` [5]_ or ``sentinel`` [6]_ PyPI packages). 136 | 137 | After researching existing idioms and implementations, and going through many 138 | different possible implementations, an implementation was written which meets 139 | all of these criteria (see `Reference Implementation`_). 140 | 141 | 142 | Specification 143 | ============= 144 | 145 | A new ``Sentinel`` class will be added to a new ``sentinels`` module. 146 | Its initializer will accept a single required argument, the name of the 147 | sentinel object, and three optional arguments: the repr of the object, its 148 | boolean value, and the name of its module:: 149 | 150 | >>> from sentinels import Sentinel 151 | >>> NotGiven = Sentinel('NotGiven') 152 | >>> NotGiven 153 | 154 | >>> MISSING = Sentinel('MISSING', repr='mymodule.MISSING') 155 | >>> MISSING 156 | mymodule.MISSING 157 | >>> MEGA = Sentinel('MEGA', 158 | repr='', 159 | bool_value=False, 160 | module_name='mymodule') 161 | 162 | 163 | Checking if a value is such a sentinel *should* be done using the ``is`` 164 | operator, as is recommended for ``None``. Equality checks using ``==`` will 165 | also work as expected, returning ``True`` only when the object is compared 166 | with itself. Identity checks such as ``if value is MISSING:`` should usually 167 | be used rather than boolean checks such as ``if value:`` or ``if not value:``. 168 | 169 | Sentinel instances are truthy by default, unlike ``None``. This parallels the 170 | default for arbitrary classes, as well as the boolean value of ``Ellipsis``. 171 | 172 | The names of sentinels are unique within each module. When calling 173 | ``Sentinel()`` in a module where a sentinel with that name was already 174 | defined, the existing sentinel with that name will be returned. Sentinels 175 | with the same name in different modules will be distinct from each other. 176 | 177 | Creating a copy of a sentinel object, such as by using ``copy.copy()`` or by 178 | pickling and unpickling, will return the same object. 179 | 180 | Type annotations for sentinel values should use ``Literal[]``. 181 | For example:: 182 | 183 | def foo(value: int | Literal[MISSING] = MISSING) -> int: 184 | ... 185 | 186 | The ``module_name`` optional argument should normally not need to be supplied, 187 | as ``Sentinel()`` will usually be able to recognize the module in which it was 188 | called. ``module_name`` should be supplied only in unusual cases when this 189 | automatic recognition does not work as intended, such as perhaps when using 190 | Jython or IronPython. This parallels the designs of ``Enum`` and 191 | ``namedtuple``. For more details, see :pep:`435`. 192 | 193 | The ``Sentinel`` class may not be sub-classed, to avoid overly-clever uses 194 | based on it, such as attempts to use it as a base for implementing singletons. 195 | It is considered important that the addition of Sentinel to the stdlib should 196 | add minimal complexity. 197 | 198 | Ordering comparisons are undefined for sentinel objects. 199 | 200 | 201 | Backwards Compatibility 202 | ======================= 203 | 204 | While not breaking existing code, adding a new "sentinels" stdlib module could 205 | cause some confusion with regard to existing modules named "sentinels", and 206 | specifically with the "sentinels" package on PyPI. 207 | 208 | The existing "sentinels" package on PyPI [10]_ appears to be abandoned, with 209 | the latest release being made on Aug. 2016. Therefore, using this name for a 210 | new stdlib module seems reasonable. 211 | 212 | If and when this PEP is accepted, it may be worth verifying if this has indeed 213 | been abandoned, and if so asking to transfer ownership to the CPython 214 | maintainers to reduce the potential for confusion with the new stdlib module. 215 | 216 | 217 | How to Teach This 218 | ================= 219 | 220 | The normal types of documentation of new stdlib modules and features, namely 221 | doc-strings, module docs and a section in "What's New", should suffice. 222 | 223 | 224 | Security Implications 225 | ===================== 226 | 227 | This proposal should have no security implications. 228 | 229 | 230 | Reference Implementation 231 | ======================== 232 | 233 | The reference implementation is found in a dedicated GitHub repo [7]_. A 234 | simplified version follows:: 235 | 236 | _registry = {} 237 | 238 | class Sentinel: 239 | """Unique sentinel values.""" 240 | 241 | def __new__(cls, name, repr=None, bool_value=True, module_name=None): 242 | name = str(name) 243 | repr = str(repr) if repr else f'<{name.split(".")[-1]}>' 244 | bool_value = bool(bool_value) 245 | if module_name is None: 246 | try: 247 | module_name = \ 248 | sys._getframe(1).f_globals.get('__name__', '__main__') 249 | except (AttributeError, ValueError): 250 | module_name = __name__ 251 | 252 | registry_key = f'{module_name}-{name}' 253 | 254 | sentinel = _registry.get(registry_key, None) 255 | if sentinel is not None: 256 | return sentinel 257 | 258 | sentinel = super().__new__(cls) 259 | sentinel._name = name 260 | sentinel._repr = repr 261 | sentinel._bool_value = bool_value 262 | sentinel._module_name = module_name 263 | 264 | return _registry.setdefault(registry_key, sentinel) 265 | 266 | def __repr__(self): 267 | return self._repr 268 | 269 | def __bool__(self): 270 | return self._bool_value 271 | 272 | def __reduce__(self): 273 | return ( 274 | self.__class__, 275 | ( 276 | self._name, 277 | self._repr, 278 | self._module_name, 279 | ), 280 | ) 281 | 282 | 283 | Rejected Ideas 284 | ============== 285 | 286 | 287 | Use ``NotGiven = object()`` 288 | --------------------------- 289 | 290 | This suffers from all of the drawbacks mentioned in the `Rationale`_ section. 291 | 292 | 293 | Add a single new sentinel value, such as ``MISSING`` or ``Sentinel`` 294 | -------------------------------------------------------------------- 295 | 296 | Since such a value could be used for various things in various places, one 297 | could not always be confident that it would never be a valid value in some use 298 | cases. On the other hand, a dedicated and distinct sentinel value can be used 299 | with confidence without needing to consider potential edge-cases. 300 | 301 | Additionally, it is useful to be able to provide a meaningful name and repr 302 | for a sentinel value, specific to the context where it is used. 303 | 304 | Finally, this was a very unpopular option in the poll [4]_, with only 12% 305 | of the votes voting for it. 306 | 307 | 308 | Use the existing ``Ellipsis`` sentinel value 309 | -------------------------------------------- 310 | 311 | This is not the original intended use of Ellipsis, though it has become 312 | increasingly common to use it to define empty class or function blocks instead 313 | of using ``pass``. 314 | 315 | Also, similar to a potential new single sentinel value, ``Ellipsis`` can't be 316 | as confidently used in all cases, unlike a dedicated, distinct value. 317 | 318 | 319 | Use a single-valued enum 320 | ------------------------ 321 | 322 | The suggested idiom is:: 323 | 324 | class NotGivenType(Enum): 325 | NotGiven = 'NotGiven' 326 | NotGiven = NotGivenType.NotGiven 327 | 328 | Besides the excessive repetition, the repr is overly long: 329 | ````. A shorter repr can be defined, at 330 | the expense of a bit more code and yet more repetition. 331 | 332 | Finally, this option was the least popular among the nine options in the 333 | poll [4]_, being the only option to receive no votes. 334 | 335 | 336 | A sentinel class decorator 337 | -------------------------- 338 | 339 | The suggested idiom is:: 340 | 341 | @sentinel(repr='') 342 | class NotGivenType: pass 343 | NotGiven = NotGivenType() 344 | 345 | While this allows for a very simple and clear implementation of the decorator, 346 | the idiom is too verbose, repetitive, and difficult to remember. 347 | 348 | 349 | Using class objects 350 | ------------------- 351 | 352 | Since classes are inherently singletons, using a class as a sentinel value 353 | makes sense and allows for a simple implementation. 354 | 355 | The simplest version of this is:: 356 | 357 | class NotGiven: pass 358 | 359 | To have a clear repr, one would need to use a meta-class:: 360 | 361 | class NotGiven(metaclass=SentinelMeta): pass 362 | 363 | ... or a class decorator:: 364 | 365 | @Sentinel 366 | class NotGiven: pass 367 | 368 | Using classes this way is unusual and could be confusing. The intention of 369 | code would be hard to understand without comments. It would also cause 370 | such sentinels to have some unexpected and undesirable behavior, such as 371 | being callable. 372 | 373 | 374 | Define a recommended "standard" idiom, without supplying an implementation 375 | -------------------------------------------------------------------------- 376 | 377 | Most common existing idioms have significant drawbacks. So far, no idiom 378 | has been found that is clear and concise while avoiding these drawbacks. 379 | 380 | Also, in the poll [4]_ on this subject, the options for recommending an 381 | idiom were unpopular, with the highest-voted option being voted for by only 382 | 25% of the voters. 383 | 384 | 385 | Additional Notes 386 | ================ 387 | 388 | * This PEP and the initial implementation are drafted in a dedicated GitHub 389 | repo [7]_. 390 | 391 | * For sentinels defined in a class scope, to avoid potential name clashes, 392 | one should use the fully-qualified name of the variable in the module. Only 393 | the part of the name after the last period will be used for the default 394 | repr. For example:: 395 | 396 | >>> class MyClass: 397 | ... NotGiven = sentinel('MyClass.NotGiven') 398 | >>> MyClass.NotGiven 399 | 400 | 401 | * One should be careful when creating sentinels in a function or method, since 402 | sentinels with the same name created by code in the same module will be 403 | identical. If distinct sentinel objects are needed, make sure to use 404 | distinct names. 405 | 406 | * There is no single desirable value for the "truthiness" of sentinels, i.e. 407 | their boolean value. It is sometimes useful for the boolean value to be 408 | ``True``, and sometimes ``False``. Of the built-in sentinels in Python, 409 | ``None`` evaluates to ``False``, while ``Ellipsis`` (a.k.a. ``...``) 410 | evaluates to ``True``. The desire for this to be set as needed came up in 411 | discussions as well. 412 | 413 | * The boolean value of ``NotImplemented`` is ``True``, but using this is 414 | deprecated since Python 3.9 (doing so generates a deprecation warning.) 415 | This deprecation is due to issues specific to ``NotImplemented``, as 416 | described in bpo-35712 [8]_. 417 | 418 | * To define multiple, related sentinel values, possibly with a defined 419 | ordering among them, one should instead use ``Enum`` or something similar. 420 | 421 | * There was a discussion on the typing-sig mailing list [9]_ about the typing 422 | for these sentinels, where different options were discussed. 423 | 424 | 425 | Open Issues 426 | =========== 427 | 428 | * **Is adding a new stdlib module the right way to go?** I could not find any 429 | existing module which seems like a logical place for this. However, adding 430 | new stdlib modules should be done judiciously, so perhaps choosing an 431 | existing module would be preferable even if it is not a perfect fit? 432 | 433 | 434 | Footnotes 435 | ========= 436 | 437 | .. [1] Python-Dev mailing list: `The repr of a sentinel `_ 438 | .. [2] Python-Dev mailing list: `"The stdlib contains tons of sentinels" `_ 439 | .. [3] `bpo-44123: Make function parameter sentinel values true singletons `_ 440 | .. [4] discuss.python.org Poll: `Sentinel Values in the Stdlib `_ 441 | .. [5] `The "sentinels" package on PyPI `_ 442 | .. [6] `The "sentinel" package on PyPI `_ 443 | .. [7] `Reference implementation at the taleinat/python-stdlib-sentinels GitHub repo `_ 444 | .. [8] `bpo-35712: Make NotImplemented unusable in boolean context `_ 445 | .. [9] `Discussion thread about type signatures for these sentinels on the typing-sig mailing list `_ 446 | .. [10] `sentinels package on PyPI `_ 447 | 448 | 449 | Copyright 450 | ========= 451 | 452 | This document is placed in the public domain or under the 453 | CC0-1.0-Universal license, whichever is more permissive. 454 | -------------------------------------------------------------------------------- /sentinels/sentinels.py: -------------------------------------------------------------------------------- 1 | import sys as _sys 2 | from threading import Lock as _Lock 3 | 4 | 5 | __all__ = ['Sentinel'] 6 | 7 | 8 | # Design and implementation decisions: 9 | # 10 | # The first implementations created a dedicated class for each instance. 11 | # However, once it was decided to use Sentinel for type signatures, there 12 | # was no longer a need for a dedicated class for each sentinel value on order 13 | # to enable strict type signatures. Since class objects consume a relatively 14 | # large amount of memory, the implementation was changed to avoid this. 15 | # 16 | # With this change, the mechanism used for unpickling/copying objects needed 17 | # to be changed too, since we could no longer count on each dedicated class 18 | # simply returning its singleton instance as before. __reduce__ can return 19 | # a string, upon which an attribute with that name is looked up in the module 20 | # and returned. However, that would have meant that pickling/copying support 21 | # would depend on the "name" argument being exactly the name of the variable 22 | # used in the module, and simply wouldn't work for sentinels created in 23 | # functions/methods. Instead, a registry for sentinels was added, where all 24 | # sentinel objects are stored keyed by their name + module name. This is used 25 | # to look up existing sentinels both during normal object creation and during 26 | # copying/unpickling. 27 | 28 | 29 | class Sentinel: 30 | """Create a unique sentinel object. 31 | 32 | *name* should be the fully-qualified name of the variable to which the 33 | return value shall be assigned. 34 | 35 | *repr*, if supplied, will be used for the repr of the sentinel object. 36 | If not provided, "" will be used (with any leading class names 37 | removed). 38 | 39 | *module_name*, if supplied, will be used instead of inspecting the call 40 | stack to find the name of the module from which 41 | """ 42 | _name: str 43 | _repr: str 44 | _module_name: str 45 | 46 | def __new__( 47 | cls, 48 | name: str, 49 | repr: str | None = None, 50 | module_name: str | None = None, 51 | ): 52 | name = str(name) 53 | repr = str(repr) if repr else f'<{name.split(".")[-1]}>' 54 | if not module_name: 55 | parent_frame = _get_parent_frame() 56 | module_name = ( 57 | parent_frame.f_globals.get('__name__', '__main__') 58 | if parent_frame is not None 59 | else __name__ 60 | ) 61 | 62 | # Include the class's module and fully qualified name in the 63 | # registry key to support sub-classing. 64 | registry_key = _sys.intern( 65 | f'{cls.__module__}-{cls.__qualname__}-{module_name}-{name}' 66 | ) 67 | sentinel = _registry.get(registry_key, None) 68 | if sentinel is not None: 69 | return sentinel 70 | sentinel = super().__new__(cls) 71 | sentinel._name = name 72 | sentinel._repr = repr 73 | sentinel._module_name = module_name 74 | with _lock: 75 | return _registry.setdefault(registry_key, sentinel) 76 | 77 | def __repr__(self): 78 | return self._repr 79 | 80 | def __reduce__(self): 81 | return ( 82 | self.__class__, 83 | ( 84 | self._name, 85 | self._repr, 86 | self._module_name, 87 | ), 88 | ) 89 | 90 | 91 | _lock = _Lock() 92 | _registry: dict[str, Sentinel] = {} 93 | 94 | 95 | # The following implementation attempts to support Python 96 | # implementations which don't support sys._getframe(2), such as 97 | # Jython and IronPython. 98 | # 99 | # The version added to the stdlib may simply return sys._getframe(2), 100 | # without the fallbacks. 101 | # 102 | # For reference, see the implementation of namedtuple: 103 | # https://github.com/python/cpython/blob/67444902a0f10419a557d0a2d3b8675c31b075a9/Lib/collections/__init__.py#L503 104 | def _get_parent_frame(): 105 | """Return the frame object for the caller's parent stack frame.""" 106 | try: 107 | # Two frames up = the parent of the function which called this. 108 | return _sys._getframe(2) 109 | except (AttributeError, ValueError): 110 | global _get_parent_frame 111 | def _get_parent_frame(): 112 | """Return the frame object for the caller's parent stack frame.""" 113 | try: 114 | raise Exception 115 | except Exception: 116 | try: 117 | return _sys.exc_info()[2].tb_frame.f_back.f_back 118 | except Exception: 119 | global _get_parent_frame 120 | def _get_parent_frame(): 121 | """Return the frame object for the caller's parent stack frame.""" 122 | return None 123 | return _get_parent_frame() 124 | return _get_parent_frame() 125 | -------------------------------------------------------------------------------- /test/test_sentinels.py: -------------------------------------------------------------------------------- 1 | from sentinels import Sentinel 2 | 3 | import copy 4 | import pickle 5 | import unittest 6 | 7 | 8 | sent1 = Sentinel('sent1') 9 | sent2 = Sentinel('sent2', repr='test_sentinels.sent2') 10 | 11 | 12 | class TestSentinel(unittest.TestCase): 13 | def setUp(self): 14 | self.sent_defined_in_function = Sentinel('defined_in_function') 15 | 16 | def test_identity(self): 17 | for sent in sent1, sent2, self.sent_defined_in_function: 18 | with self.subTest(sent=sent): 19 | self.assertIs(sent, sent) 20 | self.assertEqual(sent, sent) 21 | 22 | def test_uniqueness(self): 23 | self.assertIsNot(sent1, sent2) 24 | self.assertNotEqual(sent1, sent2) 25 | self.assertIsNot(sent1, None) 26 | self.assertNotEqual(sent1, None) 27 | self.assertIsNot(sent1, Ellipsis) 28 | self.assertNotEqual(sent1, Ellipsis) 29 | self.assertIsNot(sent1, 'sent1') 30 | self.assertNotEqual(sent1, 'sent1') 31 | self.assertIsNot(sent1, '') 32 | self.assertNotEqual(sent1, '') 33 | 34 | def test_same_object_in_same_module(self): 35 | copy1 = Sentinel('sent1') 36 | self.assertIs(copy1, sent1) 37 | copy2 = Sentinel('sent1') 38 | self.assertIs(copy2, copy1) 39 | 40 | def test_same_object_fake_module(self): 41 | copy1 = Sentinel('FOO', module_name='i.dont.exist') 42 | copy2 = Sentinel('FOO', module_name='i.dont.exist') 43 | self.assertIs(copy1, copy2) 44 | 45 | def test_unique_in_different_modules(self): 46 | other_module_sent1 = Sentinel('sent1', module_name='i.dont.exist') 47 | self.assertIsNot(other_module_sent1, sent1) 48 | 49 | def test_repr(self): 50 | self.assertEqual(repr(sent1), '') 51 | self.assertEqual(repr(sent2), 'test_sentinels.sent2') 52 | 53 | def test_type(self): 54 | self.assertIsInstance(sent1, Sentinel) 55 | self.assertIsInstance(sent2, Sentinel) 56 | 57 | def test_copy(self): 58 | self.assertIs(sent1, copy.copy(sent1)) 59 | self.assertIs(sent1, copy.deepcopy(sent1)) 60 | 61 | def test_pickle_roundtrip(self): 62 | self.assertIs(sent1, pickle.loads(pickle.dumps(sent1))) 63 | 64 | def test_bool_value(self): 65 | self.assertTrue(sent1) 66 | self.assertTrue(Sentinel('I_AM_FALSY')) 67 | 68 | def test_automatic_module_name(self): 69 | self.assertIs( 70 | Sentinel('sent1', module_name=__name__), 71 | sent1, 72 | ) 73 | self.assertIs( 74 | Sentinel('defined_in_function', module_name=__name__), 75 | self.sent_defined_in_function, 76 | ) 77 | 78 | def test_subclass(self): 79 | class FalseySentinel(Sentinel): 80 | def __bool__(self): 81 | return False 82 | subclass_sent = FalseySentinel('FOO') 83 | self.assertIs(subclass_sent, subclass_sent) 84 | self.assertEqual(subclass_sent, subclass_sent) 85 | self.assertFalse(subclass_sent) 86 | non_subclass_sent = Sentinel('FOO') 87 | self.assertIsNot(subclass_sent, non_subclass_sent) 88 | self.assertNotEqual(subclass_sent, non_subclass_sent) 89 | 90 | 91 | if __name__ == '__main__': 92 | unittest.main() 93 | --------------------------------------------------------------------------------