├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── CHANGELOG ├── LICENSE ├── README.rst ├── pyproject.toml ├── qcore ├── __init__.py ├── __init__.pyi ├── asserts.py ├── asserts.pyi ├── caching.pxd ├── caching.py ├── caching.pyi ├── debug.py ├── debug.pyi ├── decorators.pxd ├── decorators.py ├── decorators.pyi ├── disallow_inheritance.py ├── disallow_inheritance.pyi ├── enum.py ├── enum.pyi ├── errors.py ├── errors.pyi ├── events.pxd ├── events.py ├── events.pyi ├── helpers.pxd ├── helpers.py ├── helpers.pyi ├── inspectable_class.py ├── inspectable_class.pyi ├── inspection.pxd ├── inspection.py ├── inspection.pyi ├── microtime.pxd ├── microtime.py ├── microtime.pyi ├── py.typed ├── testing.py ├── testing.pyi └── tests │ ├── test_asserts.py │ ├── test_caching.py │ ├── test_debug.py │ ├── test_decorators.py │ ├── test_disallow_inheritance.py │ ├── test_enum.py │ ├── test_errors.py │ ├── test_events.py │ ├── test_examples.py │ ├── test_helpers.py │ ├── test_inspectable_class.py │ ├── test_inspection.py │ ├── test_microtime.py │ └── test_testing.py ├── requirements.txt ├── setup.py └── tox.ini /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Based on 2 | # https://github.com/pypa/cibuildwheel/blob/main/examples/github-deploy.yml 3 | 4 | name: Publish Python distributions to PyPI and TestPyPI 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build_wheels: 10 | name: Build wheels on ${{ matrix.os }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-22.04, windows-2019, macOS-12] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: actions/setup-python@v5 20 | name: Install Python 21 | with: 22 | python-version: "3.12" 23 | 24 | - name: Install cibuildwheel 25 | run: python -m pip install cibuildwheel==2.21.3 26 | 27 | - name: Build wheels 28 | run: python -m cibuildwheel --output-dir wheelhouse 29 | 30 | - uses: actions/upload-artifact@v4 31 | with: 32 | name: ${{ matrix.os }} 33 | path: ./wheelhouse/*.whl 34 | 35 | build_sdist: 36 | name: Build source distribution 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - uses: actions/setup-python@v5 42 | name: Install Python 43 | with: 44 | python-version: "3.12" 45 | 46 | - name: Install build 47 | run: python -m pip install build==1.2.2.post1 48 | 49 | - name: Build sdist 50 | run: python -m build --sdist 51 | 52 | - uses: actions/upload-artifact@v4 53 | with: 54 | name: sdist 55 | path: dist/*.tar.gz 56 | 57 | upload_pypi: 58 | needs: [build_wheels, build_sdist] 59 | name: Build and publish Python distributions to PyPI and TestPyPI 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/download-artifact@v4.1.7 63 | with: 64 | name: sdist 65 | path: dist 66 | - uses: actions/download-artifact@v4.1.7 67 | with: 68 | name: ubuntu-22.04 69 | path: dist 70 | - uses: actions/download-artifact@v4.1.7 71 | with: 72 | name: windows-2019 73 | path: dist 74 | - uses: actions/download-artifact@v4.1.7 75 | with: 76 | name: macOS-12 77 | path: dist 78 | - name: Publish distribution to PyPI 79 | if: startsWith(github.ref, 'refs/tags') 80 | uses: pypa/gh-action-pypi-publish@v1.4.2 81 | with: 82 | password: ${{ secrets.PYPI_API_TOKEN }} 83 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: qcore 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | allow-prereleases: true 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install tox tox-gh-actions wheel 25 | - name: Test with tox 26 | run: tox 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | DS_Store 2 | ld/ 3 | dist/ 4 | *.egg-info/ 5 | *.egg 6 | *.py[cod] 7 | __pycache__/ 8 | *.so 9 | *.c 10 | *.h 11 | *~ 12 | /nose* 13 | .arcconfig 14 | .tox/ 15 | .mypy_cache/ 16 | build/ 17 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | ## 1.11.1 2 | 3 | - Fix version number 4 | 5 | ## 1.11.0 6 | 7 | (Not released to PyPI because of a mistake.) 8 | 9 | - Drop support for Python 3.8; add support for Python 3.13 10 | 11 | ## 1.10.1 12 | 13 | * Add minimum version requirement (Python 3.8) to metadata 14 | * Build Python 3.12 wheels 15 | 16 | ## 1.10.0 17 | 18 | * Drop support for Python 3.7 19 | * Add support for Python 3.12 20 | * Support building with Cython 3 21 | 22 | ## 1.9.1 23 | 24 | * Pin to `Cython<3` to fix build 25 | 26 | ## 1.9.0 27 | 28 | * Drop support for Python 3.6; add support for Python 3.11 29 | * Add functions `utime_as_datetime`, `datetime_as_utime`, 30 | `format_utime_as_iso_8601`, `iso_8601_as_utime` 31 | 32 | ## 1.8.0 33 | 34 | * Add `.fn` attributes to cache functions in `qcore.caching`, 35 | enabling pyanalyze type checking 36 | * Remove broken caching from qcore.inspection.get_full_name 37 | * Add support for Python 3.10 38 | * Support dict subclasses in `qcore.asserts.assert_dict_eq` 39 | * Fix `**kwargs` support in `qcore.caching.cached_per_instance` 40 | * Use ASCII representation of objects in error messages 41 | * Fix type annotation for `qcore.caching.LRUCache.get` 42 | 43 | ## 1.7.0 44 | 45 | * Update mypy version 46 | * Drop support for Python 2.7, 3.4, and 3.5 47 | * Add support for Python 3.9 48 | * Build wheels using GitHub actions and cibuildwheel 49 | * Improve stub files 50 | * Prevent AttributeError in qcore.inspection.get_original_fn 51 | * Use relative imports in pxd files 52 | 53 | ## 1.6.1 54 | 55 | * Support Python 3.8 56 | 57 | ## 1.6.0 58 | 59 | * Optimize import time 60 | * Add to AssertRaises stubs 61 | * Add assert_startswith and assert_endswith 62 | 63 | ## 1.5.0 64 | 65 | * Fix stub for decorator_of_context_manager 66 | * Add qcore.Utime typing helper 67 | * Add clear() method to LRUCache 68 | * Fix stub for LRUCache 69 | 70 | ## 0.5.1 71 | * Add __prepare__ to some metaclasses to fix errors with six 1.11.0. 72 | 73 | ## 0.5.0 74 | * Start publishing manylinux wheels 75 | * Support Python 3.7 76 | * Improve pickle implementation for enums 77 | * Add type stubs 78 | 79 | ## 0.4.2 80 | * Improve installation procedure 81 | 82 | ## 0.4.1 83 | * Add qcore.caching.lru_cache 84 | * Declare Cython as a setup_requires dependency 85 | 86 | ## 0.4.0 87 | * Add support for some new features in py3 (e.g. keyword-only args). 88 | * Make pickling/unpickling of enums work across py2 and py3. 89 | * Make helpers.object_from_string handle more cases. 90 | 91 | ## 0.3.0 92 | * Various enum improvements. Invalid values are now disallowed, methods like 93 | get_names() return members in consistent order, there is a new IntEnum 94 | class, and some new methods were added. 95 | * Cython is no longer required to import qcore if it has not been compiled with 96 | Cython. 97 | * @qcore.caching.memoize() is now more efficient. 98 | 99 | ## 0.2.1 100 | * MarkerObject names are now always text 101 | * Fix equality for DecoratorBinder objects to work correctly in compiled and 102 | non-compiled Python 2 and 3. 103 | * Errors run during event handlers are now reraised with a meaningful 104 | traceback. 105 | 106 | ## 0.2.0 107 | * Public release 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ***** 2 | qcore 3 | ***** 4 | 5 | ``qcore`` is a library of common utility functions used at Quora. It is used to 6 | abstract out common functionality for other Quora libraries like `asynq `_. 7 | 8 | Its component modules are discussed below. See the docstrings in the code 9 | itself for more detail. 10 | 11 | qcore.asserts 12 | ------------- 13 | 14 | When a normal Python assert fails, it only indicates that there was a failure, 15 | not what the bad values were that caused the assert to fail. This module 16 | provides rich assertion helpers that automatically produce better error 17 | messages. For example: 18 | 19 | .. code-block:: python 20 | 21 | >>> from qcore.asserts import assert_eq 22 | >>> assert 5 == 2 * 2 23 | Traceback (most recent call last): 24 | File "", line 1, in 25 | AssertionError 26 | >>> assert_eq(5, 2 * 2) 27 | Traceback (most recent call last): 28 | File "", line 1, in 29 | File "qcore/asserts.py", line 82, in assert_eq 30 | assert expected == actual, _assert_fail_message(message, expected, actual, '!=', extra) 31 | AssertionError: 5 != 4 32 | 33 | Similar methods are provided by the standard library's ``unittest`` package, 34 | but those are tied to the ``TestCase`` class instead of being standalone 35 | functions. 36 | 37 | qcore.caching 38 | ------------- 39 | 40 | This provides helpers for caching data. Some examples include: 41 | 42 | .. code-block:: python 43 | 44 | from qcore.caching import cached_per_instance, lazy_constant 45 | 46 | @lazy_constant 47 | def some_function(): 48 | # this will only be executed the first time some_function() is called; 49 | # afterwards it will be cached 50 | return expensive_computation() 51 | 52 | class SomeClass: 53 | @cached_per_instance() 54 | def some_method(self, a, b): 55 | # for any instance of SomeClass, this will only be executed once 56 | return expensive_computation(a, b) 57 | 58 | qcore.debug 59 | ----------- 60 | 61 | This module provides some helpers useful for debugging Python. Among others, it 62 | includes the ``@qcore.debug.trace()`` decorator, which can be used to trace 63 | every time a function is called. 64 | 65 | qcore.decorators 66 | ---------------- 67 | 68 | This module provides an abstraction for class-based decorators that supports 69 | transparently decorating functions, methods, classmethods, and staticmethods 70 | while also providing the option to add additional custom attributes. For 71 | example, it could be used to provide a caching decorator that adds a ``.dirty`` 72 | attribute to decorated functions to dirty their cache: 73 | 74 | .. code-block:: python 75 | 76 | from qcore.decorators import DecoratorBase, DecoratorBinder, decorate 77 | 78 | class CacheDecoratorBinder(DecoratorBinder): 79 | def dirty(self, *args): 80 | if self.instance is None: 81 | return self.decorator.dirty(*args) 82 | else: 83 | return self.decorator.dirty(self.instance, *args) 84 | 85 | class CacheDecorator(DecoratorBase): 86 | binder_cls = CacheDecoratorBinder 87 | 88 | def __init__(self, *args): 89 | super().__init__(*args) 90 | self._cache = {} 91 | 92 | def dirty(self, *args): 93 | try: 94 | del self._cache[args] 95 | except KeyError: 96 | pass 97 | 98 | def __call__(self, *args): 99 | try: 100 | return self._cache[args] 101 | except KeyError: 102 | value = self.fn(*args) 103 | self._cache[args] = value 104 | return value 105 | 106 | cached = decorate(CacheDecorator) 107 | 108 | qcore.enum 109 | ---------- 110 | 111 | This module provides an abstraction for defining enums. You can define an enum 112 | as follows: 113 | 114 | .. code-block:: python 115 | 116 | from qcore.enum import Enum 117 | 118 | class Color(Enum): 119 | red = 1 120 | green = 2 121 | blue = 3 122 | 123 | qcore.errors 124 | ------------ 125 | 126 | This module provides some commonly useful exception classes and helpers for 127 | reraising exceptions from a different place. 128 | 129 | qcore.events 130 | ------------ 131 | 132 | This provides an abstraction for registering events and running callbacks. 133 | Example usage: 134 | 135 | .. code-block:: python 136 | 137 | >>> from qcore.events import EventHook 138 | >>> event = EventHook() 139 | >>> def callback(): 140 | ... print('callback called') 141 | ... 142 | >>> event.subscribe(callback) 143 | >>> event.trigger() 144 | callback called 145 | 146 | qcore.helpers 147 | ------------- 148 | 149 | This provides a number of small helper functions. 150 | 151 | qcore.inspectable_class 152 | ----------------------- 153 | 154 | This provides a base class that automatically provides hashing, equality 155 | checks, and a readable ``repr()`` result. Example usage: 156 | 157 | .. code-block:: python 158 | 159 | >>> from qcore.inspectable_class import InspectableClass 160 | >>> class Pair(InspectableClass): 161 | ... def __init__(self, a, b): 162 | ... self.a = a 163 | ... self.b = b 164 | ... 165 | >>> Pair(1, 2) 166 | Pair(a=1, b=2) 167 | >>> Pair(1, 2) == Pair(1, 2) 168 | True 169 | 170 | qcore.inspection 171 | ---------------- 172 | 173 | This provides functionality similar to the standard ``inspect`` module. Among 174 | others, it includes the ``get_original_fn`` function, which extracts the 175 | underlying function from a ``qcore.decorators``-decorated object. 176 | 177 | qcore.microtime 178 | --------------- 179 | 180 | This includes helpers for dealing with time, represented as an integer number 181 | of microseconds since the Unix epoch. 182 | 183 | qcore.testing 184 | ------------- 185 | 186 | This provides helpers to use in unit tests. Among others, it provides an 187 | ``Anything`` object that compares equal to any other Python object. 188 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "qcore" 3 | version = "1.11.1" 4 | requires-python = ">=3.9" 5 | keywords = ["quora", "core", "common", "utility"] 6 | classifiers = [ 7 | "License :: OSI Approved :: Apache Software License", 8 | "Programming Language :: Python", 9 | "Programming Language :: Python :: 3.9", 10 | "Programming Language :: Python :: 3.10", 11 | "Programming Language :: Python :: 3.11", 12 | "Programming Language :: Python :: 3.12", 13 | "Programming Language :: Python :: 3.13", 14 | ] 15 | authors = [ 16 | {name = "Quora, Inc.", email = "asynq@quora.com"}, 17 | ] 18 | license = {text = "Apache Software License"} 19 | dynamic = ["readme"] 20 | 21 | [tool.black] 22 | target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] 23 | include = '\.pyi?$' 24 | skip-magic-trailing-comma = true 25 | preview = true 26 | 27 | exclude = ''' 28 | /( 29 | \.git 30 | | \.mypy_cache 31 | | \.tox 32 | | \.venv 33 | | \.eggs 34 | )/ 35 | ''' 36 | 37 | [tool.mypy] 38 | python_version = "3.9" 39 | warn_unused_configs = true 40 | 41 | [build-system] 42 | requires = ["setuptools>=64.0", "cython>=3"] 43 | build-backend = "setuptools.build_meta" 44 | 45 | [tool.cibuildwheel] 46 | build = "cp{39,310,311,312,313}-*" 47 | -------------------------------------------------------------------------------- /qcore/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from . import inspection 16 | from .inspection import get_original_fn, get_full_name 17 | from .errors import * 18 | from .helpers import * 19 | from .enum import * 20 | from .microtime import * 21 | from . import events 22 | from .events import ( 23 | EventHook, 24 | EventHub, 25 | EnumBasedEventHub, 26 | EventInterceptor, 27 | sinking_event_hook, 28 | ) 29 | from .decorators import * 30 | from .caching import * 31 | from . import debug 32 | from . import testing 33 | from . import asserts 34 | from .inspectable_class import InspectableClass 35 | from .disallow_inheritance import DisallowInheritance 36 | -------------------------------------------------------------------------------- /qcore/__init__.pyi: -------------------------------------------------------------------------------- 1 | from . import inspection 2 | from .inspection import ( 3 | get_original_fn as get_original_fn, 4 | get_full_name as get_full_name, 5 | ) 6 | from .errors import * 7 | from .helpers import * 8 | from .enum import * 9 | from .microtime import * 10 | from . import events 11 | from .events import ( 12 | EventHook as EventHook, 13 | EventHub as EventHub, 14 | EnumBasedEventHub as EnumBasedEventHub, 15 | EventInterceptor as EventInterceptor, 16 | sinking_event_hook as sinking_event_hook, 17 | ) 18 | from .decorators import * 19 | from .caching import * 20 | from . import debug 21 | from . import testing 22 | from . import asserts 23 | from .inspectable_class import InspectableClass as InspectableClass 24 | from .disallow_inheritance import DisallowInheritance as DisallowInheritance 25 | -------------------------------------------------------------------------------- /qcore/asserts.pyi: -------------------------------------------------------------------------------- 1 | from types import TracebackType 2 | from typing import ( 3 | Any, 4 | Callable, 5 | Container, 6 | Dict, 7 | Iterable, 8 | List, 9 | Optional, 10 | Set, 11 | Tuple, 12 | Type, 13 | Union, 14 | ) 15 | 16 | _Numeric = Union[int, float, complex] 17 | 18 | def assert_is( 19 | expected: Any, 20 | actual: Any, 21 | message: Optional[str] = ..., 22 | extra: Optional[object] = ..., 23 | ) -> None: ... 24 | def assert_is_not( 25 | expected: Any, 26 | actual: Any, 27 | message: Optional[str] = ..., 28 | extra: Optional[object] = ..., 29 | ) -> None: ... 30 | def assert_is_instance( 31 | value: Any, 32 | types: Union[Type[Any], Tuple[Union[Type[Any], Tuple[Any, ...]], ...]], 33 | message: Optional[str] = ..., 34 | extra: Optional[object] = ..., 35 | ) -> None: ... 36 | def assert_eq( 37 | expected: Any, 38 | actual: Any, 39 | message: Optional[str] = ..., 40 | tolerance: Optional[_Numeric] = ..., 41 | extra: Optional[object] = ..., 42 | ) -> None: ... 43 | def assert_dict_eq( 44 | expected: Dict[Any, Any], 45 | actual: Dict[Any, Any], 46 | number_tolerance: Optional[_Numeric] = ..., 47 | dict_path: List[Any] = ..., 48 | ) -> None: ... 49 | def assert_ne( 50 | expected: Any, 51 | actual: Any, 52 | message: Optional[str] = ..., 53 | tolerance: Optional[_Numeric] = ..., 54 | extra: Optional[object] = ..., 55 | ) -> None: ... 56 | def assert_gt( 57 | expected: Any, 58 | actual: Any, 59 | message: Optional[str] = ..., 60 | extra: Optional[object] = ..., 61 | ) -> None: ... 62 | def assert_ge( 63 | expected: Any, 64 | actual: Any, 65 | message: Optional[str] = ..., 66 | extra: Optional[object] = ..., 67 | ) -> None: ... 68 | def assert_lt( 69 | expected: Any, 70 | actual: Any, 71 | message: Optional[str] = ..., 72 | extra: Optional[object] = ..., 73 | ) -> None: ... 74 | def assert_le( 75 | expected: Any, 76 | actual: Any, 77 | message: Optional[str] = ..., 78 | extra: Optional[object] = ..., 79 | ) -> None: ... 80 | def assert_in( 81 | expected: Any, 82 | actual: Container[Any], 83 | message: Optional[str] = ..., 84 | extra: Optional[object] = ..., 85 | ) -> None: ... 86 | def assert_not_in( 87 | expected: Any, 88 | actual: Container[Any], 89 | message: Optional[str] = ..., 90 | extra: Optional[object] = ..., 91 | ) -> None: ... 92 | def assert_in_with_tolerance( 93 | expected: Any, 94 | actual: Container[Any], 95 | tolerance: _Numeric, 96 | message: Optional[str] = ..., 97 | extra: Optional[object] = ..., 98 | ) -> None: ... 99 | def assert_unordered_list_eq( 100 | expected: Iterable[Any], actual: Iterable[Any], message: Optional[str] = ... 101 | ) -> None: ... 102 | def assert_raises( 103 | fn: Callable[[], Any], *expected_exception_types: Type[BaseException] 104 | ) -> None: ... 105 | 106 | class AssertRaises: 107 | expected_exception_types: Set[Type[BaseException]] 108 | expected_exception_found: Any 109 | extra: Optional[str] 110 | def __init__( 111 | self, 112 | *expected_exception_types: Type[BaseException], 113 | extra: Optional[object] = ..., 114 | ) -> None: ... 115 | def __enter__(self) -> AssertRaises: ... 116 | def __exit__( 117 | self, 118 | exc_type: Optional[Type[BaseException]], 119 | exc_val: Optional[BaseException], 120 | exc_tb: Optional[TracebackType], 121 | ) -> bool: ... 122 | 123 | # =================================================== 124 | # Strings 125 | # =================================================== 126 | 127 | def assert_is_substring( 128 | substring: str, 129 | subject: str, 130 | message: Optional[str] = ..., 131 | extra: Optional[object] = ..., 132 | ) -> None: ... 133 | def assert_is_not_substring( 134 | substring: str, 135 | subject: str, 136 | message: Optional[str] = ..., 137 | extra: Optional[object] = ..., 138 | ) -> None: ... 139 | def assert_startswith( 140 | prefix: str, 141 | subject: str, 142 | message: Optional[str] = ..., 143 | extra: Optional[object] = ..., 144 | ) -> None: ... 145 | def assert_endswith( 146 | suffix: str, 147 | subject: str, 148 | message: Optional[str] = ..., 149 | extra: Optional[object] = ..., 150 | ) -> None: ... 151 | -------------------------------------------------------------------------------- /qcore/caching.pxd: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import cython 16 | 17 | from . cimport helpers 18 | 19 | 20 | cdef object miss 21 | cdef object not_computed 22 | 23 | 24 | cdef class LazyConstant: 25 | cdef public object value_provider 26 | cdef public object value 27 | 28 | cpdef get_value(self) 29 | cpdef compute(self) 30 | cpdef clear(self) 31 | 32 | 33 | cdef class LRUCache: 34 | cdef int _capacity 35 | cdef object _item_evicted 36 | cdef object _dict 37 | 38 | cpdef get_capacity(self) 39 | cpdef get(self, key, default=?) 40 | cpdef clear(self, omit_item_evicted=?) 41 | 42 | cdef _evict_item(self, object key, object value) 43 | cdef inline _update_item(self, object key, object value) 44 | 45 | 46 | @cython.locals(args_list=list, args_len=int, all_args_len=int, arg_name=str) 47 | cpdef get_args_tuple(tuple args, dict kwargs, list arg_names, dict kwargs_defaults) 48 | -------------------------------------------------------------------------------- /qcore/caching.pyi: -------------------------------------------------------------------------------- 1 | import threading 2 | import inspect 3 | from typing import ( 4 | Any, 5 | Callable, 6 | Dict, 7 | Generic, 8 | Iterable, 9 | Iterator, 10 | List, 11 | Mapping, 12 | MutableMapping, 13 | overload, 14 | Sequence, 15 | Tuple, 16 | TypeVar, 17 | Optional, 18 | Union, 19 | ) 20 | 21 | from . import helpers 22 | from .helpers import miss as miss 23 | 24 | _ArgSpec = Union[inspect.ArgSpec, inspect.FullArgSpec] 25 | 26 | _T = TypeVar("_T") 27 | _KT = TypeVar("_KT") 28 | _VT = TypeVar("_VT") 29 | _CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) 30 | 31 | not_computed: helpers.MarkerObject 32 | 33 | class LazyConstant(Generic[_T]): 34 | value_provider: Callable[[], _T] 35 | value: Union[helpers.MarkerObject, _T] 36 | def __init__(self, value_provider: Callable[[], _T]) -> None: ... 37 | def get_value(self) -> Optional[_T]: ... 38 | def compute(self) -> Optional[_T]: ... 39 | def clear(self) -> None: ... 40 | 41 | class _NewLazyConstant(LazyConstant[_T]): 42 | def __call__(self) -> _T: ... 43 | 44 | def lazy_constant(fn: Callable[[], _T]) -> _NewLazyConstant[_T]: ... 45 | 46 | class ThreadLocalLazyConstant(threading.local, Generic[_T]): 47 | value_provider: Callable[[], _T] 48 | value: Union[helpers.MarkerObject, _T] 49 | def __init__(self, value_provider: Callable[[], _T]) -> None: ... 50 | def get_value(self) -> Optional[_T]: ... 51 | def compute(self) -> Optional[_T]: ... 52 | def clear(self) -> None: ... 53 | 54 | class LRUCache(MutableMapping[_KT, _VT]): 55 | def __init__( 56 | self, capacity: int, item_evicted: Optional[Callable[[_KT, _VT], object]] = ... 57 | ) -> None: ... 58 | def get_capacity(self) -> int: ... 59 | def __len__(self) -> int: ... 60 | def __contains__(self, key: object) -> bool: ... 61 | def __getitem__(self, key: _KT) -> _VT: ... 62 | def __iter__(self) -> Iterator[_KT]: ... 63 | @overload # type: ignore 64 | # returns either _VT or qcore.miss but we can't write Literal[miss] 65 | def get(self, key: _KT) -> Union[_VT, Any]: ... 66 | @overload 67 | def get(self, key: _KT, default: _T) -> Union[_VT, _T]: ... 68 | def __setitem__(self, key: _KT, value: _VT) -> None: ... 69 | def __delitem__(self, key: _KT) -> None: ... 70 | def clear(self, omit_item_evicted: bool = ...) -> None: ... 71 | 72 | def lru_cache( 73 | maxsize: int = ..., 74 | key_fn: Optional[Callable[[List[Any], Dict[str, Any]], object]] = ..., 75 | ) -> Callable[[_CallableT], _CallableT]: ... 76 | def cached_per_instance() -> Callable[[_CallableT], _CallableT]: ... 77 | def get_args_tuple( 78 | args: Iterable[object], 79 | kwargs: Mapping[str, object], 80 | arg_names: Sequence[str], 81 | kwargs_defaults: Mapping[str, object], 82 | ) -> Tuple[object, ...]: ... 83 | def get_kwargs_defaults(argspec: _ArgSpec) -> Dict[str, object]: ... 84 | def memoize(fun: _CallableT) -> _CallableT: ... 85 | def memoize_with_ttl(ttl_secs: int = ...) -> Callable[[_CallableT], _CallableT]: ... 86 | -------------------------------------------------------------------------------- /qcore/debug.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | 17 | Helpers for debugging. 18 | 19 | """ 20 | 21 | import inspect 22 | import time 23 | import traceback 24 | 25 | from . import inspection 26 | 27 | 28 | counters = {} 29 | globals()["counters"] = counters 30 | 31 | 32 | def trace(enter=False, exit=True): 33 | """ 34 | This decorator prints entry and exit message when 35 | the decorated method is called, as well as call 36 | arguments, result and thrown exception (if any). 37 | 38 | :param enter: indicates whether entry message should be printed. 39 | :param exit: indicates whether exit message should be printed. 40 | :return: decorated function. 41 | 42 | """ 43 | 44 | def decorate(fn): 45 | @inspection.wraps(fn) 46 | def new_fn(*args, **kwargs): 47 | name = fn.__module__ + "." + fn.__name__ 48 | if enter: 49 | print( 50 | "%s(args = %s, kwargs = %s) <-" % (name, repr(args), repr(kwargs)) 51 | ) 52 | try: 53 | result = fn(*args, **kwargs) 54 | if exit: 55 | print( 56 | "%s(args = %s, kwargs = %s) -> %s" 57 | % (name, repr(args), repr(kwargs), repr(result)) 58 | ) 59 | return result 60 | except Exception as e: 61 | if exit: 62 | print( 63 | "%s(args = %s, kwargs = %s) -> thrown %s" 64 | % (name, repr(args), repr(kwargs), str(e)) 65 | ) 66 | raise 67 | 68 | return new_fn 69 | 70 | return decorate 71 | 72 | 73 | class DebugCounter: 74 | def __init__(self, name, value=0): 75 | self.name = name 76 | self.value = value 77 | self.last_dump_time = 0 78 | counters[name] = self 79 | 80 | def increment(self, increment_by=1): 81 | self.value += increment_by 82 | return self 83 | 84 | def decrement(self, decrement_by=1): 85 | self.value -= decrement_by 86 | return self 87 | 88 | def dump(self): 89 | self.last_dump_time = time.time() 90 | print("debug: " + str(self)) 91 | return self 92 | 93 | def dump_if(self, predicate, and_break=False): 94 | if predicate(self): 95 | self.dump() 96 | if and_break: 97 | breakpoint() 98 | return self 99 | 100 | def dump_every(self, interval_in_seconds=1): 101 | if self.last_dump_time + interval_in_seconds < time.time(): 102 | self.dump() 103 | return self 104 | 105 | def break_if(self, predicate): 106 | if predicate(self): 107 | breakpoint() 108 | return self 109 | 110 | def __str__(self): 111 | return "DebugCounter(%s, value=%d)" % (repr(self.name), self.value) 112 | 113 | def __repr__(self): 114 | return self.__str__() 115 | 116 | 117 | def counter(name): 118 | global counters 119 | if name in counters: 120 | return counters[name] 121 | else: 122 | return DebugCounter(name) 123 | 124 | 125 | def breakpoint(): 126 | print("Breakpoint reached.") 127 | 128 | 129 | def hang_me(timeout_secs=10000): 130 | """Used for debugging tests.""" 131 | print("Sleeping. Press Ctrl-C to continue...") 132 | try: 133 | time.sleep(timeout_secs) 134 | except KeyboardInterrupt: 135 | print("Done sleeping") 136 | 137 | 138 | def format_stack(): 139 | return "".join(traceback.format_stack()) 140 | 141 | 142 | def get_bool_by_mask(source, prefix): 143 | result = True 144 | for k in dir(source): 145 | v = getattr(source, k, None) 146 | if k.startswith(prefix) and type(v) is bool: 147 | result = result and v 148 | return result 149 | 150 | 151 | def set_by_mask(target, prefix, value): 152 | for k in dir(target): 153 | v = getattr(target, k, None) 154 | if k.startswith(prefix) and not inspect.isfunction(v): 155 | try: 156 | setattr(target, k, value) 157 | except AttributeError: 158 | pass 159 | -------------------------------------------------------------------------------- /qcore/debug.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Dict, TypeVar 2 | 3 | _CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) 4 | _SelfT = TypeVar("_SelfT", bound=DebugCounter) 5 | 6 | counters: Dict[str, DebugCounter] 7 | 8 | def trace( 9 | enter: bool = ..., exit: bool = ... 10 | ) -> Callable[[_CallableT], _CallableT]: ... 11 | 12 | class DebugCounter: 13 | def __init__(self, name: str, value: int = ...) -> None: ... 14 | def increment(self: _SelfT, increment_by: int = ...) -> _SelfT: ... 15 | def decrement(self: _SelfT, decrement_by: int = ...) -> _SelfT: ... 16 | def dump(self: _SelfT) -> _SelfT: ... 17 | def dump_if( 18 | self: _SelfT, predicate: Callable[[_SelfT], bool], and_break: bool = ... 19 | ) -> _SelfT: ... 20 | def dump_every(self: _SelfT, interval_in_seconds: int = ...) -> _SelfT: ... 21 | def break_if(self: _SelfT, predicate: Callable[[_SelfT], bool]) -> _SelfT: ... 22 | 23 | def counter(name: str) -> DebugCounter: ... 24 | def breakpoint() -> None: ... 25 | def hang_me(timeout_secs: int = ...) -> None: ... 26 | def format_stack() -> str: ... 27 | def get_bool_by_mask(source: object, prefix: str) -> bool: ... 28 | def set_by_mask(target: object, prefix: str, value: object) -> None: ... 29 | -------------------------------------------------------------------------------- /qcore/decorators.pxd: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import cython 16 | 17 | 18 | cdef class DecoratorBase: 19 | cdef public object fn 20 | cdef public object type 21 | 22 | cpdef str name(self) 23 | cpdef bint is_decorator(self) except -1 24 | 25 | cdef class DecoratorBinder: 26 | cdef public DecoratorBase decorator 27 | cdef public object instance 28 | 29 | cpdef str name(self) 30 | cpdef bint is_decorator(self) except -1 31 | 32 | 33 | cdef inline void _update_wrapper(object wrapper, object wrapped) 34 | -------------------------------------------------------------------------------- /qcore/decorators.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | 17 | Base classes and helpers for decorators. 18 | 19 | """ 20 | 21 | __all__ = [ 22 | "DecoratorBinder", 23 | "DecoratorBase", 24 | "decorate", 25 | "deprecated", 26 | "convert_result", 27 | "retry", 28 | "decorator_of_context_manager", 29 | ] 30 | 31 | import functools 32 | import inspect 33 | import sys 34 | import time 35 | 36 | from . import inspection 37 | 38 | 39 | class DecoratorBinder: 40 | def __init__(self, decorator, instance=None): 41 | self.decorator = decorator 42 | self.instance = instance 43 | 44 | def name(self): 45 | return self.decorator.name() 46 | 47 | def is_decorator(self): 48 | return True 49 | 50 | def __call__(self, *args, **kwargs): 51 | if self.instance is None: 52 | return self.decorator(*args, **kwargs) 53 | else: 54 | return self.decorator(self.instance, *args, **kwargs) 55 | 56 | def __str__(self): 57 | if self.instance is None: 58 | return "<%s unbound>" % str(self.decorator) 59 | else: 60 | return "<%s bound to %s>" % (str(self.decorator), str(self.instance)) 61 | 62 | def __repr__(self): 63 | return self.__str__() 64 | 65 | def __eq__(self, other): 66 | return ( 67 | self.__class__ is other.__class__ 68 | and self.decorator == other.decorator 69 | and self.instance == other.instance 70 | ) 71 | 72 | def __hash__(self): 73 | return hash(self.decorator) ^ hash(self.instance) 74 | 75 | 76 | class DecoratorBase: 77 | binder_cls = DecoratorBinder 78 | 79 | def __init__(self, fn): 80 | if hasattr(fn, "__func__"): # Class method, static method 81 | self.type = type(fn) 82 | fn = fn.__func__ 83 | elif hasattr(fn, "is_decorator"): # Decorator 84 | self.type = fn.type 85 | else: 86 | self.type = None 87 | self.fn = fn 88 | _update_wrapper(self, fn) 89 | 90 | def name(self): 91 | raise NotImplementedError() 92 | 93 | def is_decorator(self): 94 | return True 95 | 96 | def __call__(self, *args, **kwargs): 97 | raise NotImplementedError() 98 | 99 | def __get__(self, owner, cls): 100 | if self.type is staticmethod: 101 | return self 102 | # unbound method being acccessed directly on the class 103 | if owner is None and self.type is not classmethod: 104 | return self.binder_cls(self) 105 | return self.binder_cls(self, cls if self.type is classmethod else owner) 106 | 107 | def __str__(self): 108 | return self.name() + " " + inspection.get_full_name(self.fn) 109 | 110 | def __repr__(self): 111 | return self.__str__() 112 | 113 | def __reduce__(self): 114 | # For pickling. We assume that the decorated function is available in its module's global 115 | # scope. Alternatively, we could supply type(self) and the decorator class's __init__ 116 | # arguments, but that runs into "it's not the same object" errors from Pickle. 117 | return (_reduce_impl, (self.__module__, self.__name__)) 118 | 119 | 120 | # We use wrappers of Cython extension classes here to enable 121 | # Python features like adding new properties for decorators. 122 | _wrappers = {} 123 | 124 | 125 | def decorate(decorator_cls, *args, **kwargs): 126 | """Creates a decorator function that applies the decorator_cls that was passed in.""" 127 | global _wrappers 128 | 129 | wrapper_cls = _wrappers.get(decorator_cls, None) 130 | if wrapper_cls is None: 131 | 132 | class PythonWrapper(decorator_cls): 133 | pass 134 | 135 | wrapper_cls = PythonWrapper 136 | wrapper_cls.__name__ = decorator_cls.__name__ + "PythonWrapper" 137 | _wrappers[decorator_cls] = wrapper_cls 138 | 139 | def decorator(fn): 140 | wrapped = wrapper_cls(fn, *args, **kwargs) 141 | _update_wrapper(wrapped, fn) 142 | return wrapped 143 | 144 | return decorator 145 | 146 | 147 | def deprecated(replacement_description): 148 | """States that method is deprecated. 149 | 150 | :param replacement_description: Describes what must be used instead. 151 | :return: the original method with modified docstring. 152 | 153 | """ 154 | 155 | def decorate(fn_or_class): 156 | if isinstance(fn_or_class, type): 157 | pass # Can't change __doc__ of type objects 158 | else: 159 | try: 160 | fn_or_class.__doc__ = "This API point is obsolete. %s\n\n%s" % ( 161 | replacement_description, 162 | fn_or_class.__doc__, 163 | ) 164 | except AttributeError: 165 | pass # For Cython method descriptors, etc. 166 | return fn_or_class 167 | 168 | return decorate 169 | 170 | 171 | def convert_result(converter): 172 | """Decorator that can convert the result of a function call.""" 173 | 174 | def decorate(fn): 175 | @inspection.wraps(fn) 176 | def new_fn(*args, **kwargs): 177 | return converter(fn(*args, **kwargs)) 178 | 179 | return new_fn 180 | 181 | return decorate 182 | 183 | 184 | def retry(exception_cls, max_tries=10, sleep=0.05): 185 | """Decorator for retrying a function if it throws an exception. 186 | 187 | :param exception_cls: an exception type or a parenthesized tuple of exception types 188 | :param max_tries: maximum number of times this function can be executed. Must be at least 1. 189 | :param sleep: number of seconds to sleep between function retries 190 | 191 | """ 192 | 193 | assert max_tries > 0 194 | 195 | def with_max_retries_call(delegate): 196 | for i in range(max_tries): 197 | try: 198 | return delegate() 199 | except exception_cls: 200 | if i + 1 == max_tries: 201 | raise 202 | time.sleep(sleep) 203 | 204 | def outer(fn): 205 | is_generator = inspect.isgeneratorfunction(fn) 206 | 207 | @functools.wraps(fn) 208 | def retry_fun(*args, **kwargs): 209 | return with_max_retries_call(lambda: fn(*args, **kwargs)) 210 | 211 | @functools.wraps(fn) 212 | def retry_generator_fun(*args, **kwargs): 213 | def get_first_item(): 214 | results = fn(*args, **kwargs) 215 | for first_result in results: 216 | return [first_result], results 217 | return [], results 218 | 219 | cache, generator = with_max_retries_call(get_first_item) 220 | 221 | for item in cache: 222 | yield item 223 | 224 | for item in generator: 225 | yield item 226 | 227 | if not is_generator: 228 | # so that qcore.inspection.get_original_fn can retrieve the original function 229 | retry_fun.fn = fn 230 | # Necessary for pickling of Cythonized functions to work. Cython's __reduce__ 231 | # method always returns the original name of the function. 232 | retry_fun.__reduce__ = lambda: fn.__name__ 233 | return retry_fun 234 | else: 235 | retry_generator_fun.fn = fn 236 | retry_generator_fun.__reduce__ = lambda: fn.__name__ 237 | return retry_generator_fun 238 | 239 | return outer 240 | 241 | 242 | def decorator_of_context_manager(ctxt): 243 | """Converts a context manager into a decorator. 244 | 245 | This decorator will run the decorated function in the context of the 246 | manager. 247 | 248 | :param ctxt: Context to run the function in. 249 | :return: Wrapper around the original function. 250 | 251 | """ 252 | 253 | def decorator_fn(*outer_args, **outer_kwargs): 254 | def decorator(fn): 255 | @functools.wraps(fn) 256 | def wrapper(*args, **kwargs): 257 | with ctxt(*outer_args, **outer_kwargs): 258 | return fn(*args, **kwargs) 259 | 260 | return wrapper 261 | 262 | return decorator 263 | 264 | if getattr(ctxt, "__doc__", None) is None: 265 | msg = "Decorator that runs the inner function in the context of %s" 266 | decorator_fn.__doc__ = msg % ctxt 267 | else: 268 | decorator_fn.__doc__ = ctxt.__doc__ 269 | return decorator_fn 270 | 271 | 272 | def _update_wrapper(wrapper, wrapped): 273 | if hasattr(wrapped, "__module__"): 274 | wrapper.__module__ = wrapped.__module__ 275 | if hasattr(wrapped, "__name__"): 276 | wrapper.__name__ = wrapped.__name__ 277 | if hasattr(wrapped, "__doc__"): 278 | wrapper.__doc__ = wrapped.__doc__ 279 | 280 | 281 | def _reduce_impl(module, name): 282 | try: 283 | module = sys.modules[module] 284 | return getattr(module, name) 285 | except (KeyError, AttributeError): 286 | raise TypeError( 287 | "Cannot pickle decorated function %s.%s, failed to find it" % (module, name) 288 | ) 289 | -------------------------------------------------------------------------------- /qcore/decorators.pyi: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import ( 3 | Any, 4 | Callable, 5 | ContextManager, 6 | Generic, 7 | Optional, 8 | Tuple, 9 | Type, 10 | TypeVar, 11 | Union, 12 | ) 13 | 14 | _CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) 15 | _T = TypeVar("_T") 16 | 17 | class DecoratorBinder(Generic[_T]): 18 | decorator: DecoratorBase[_T] 19 | instance: Optional[object] 20 | def __init__( 21 | self, decorator: DecoratorBase[_T], instance: Optional[object] = ... 22 | ) -> None: ... 23 | def name(self) -> str: ... 24 | def is_decorator(self) -> bool: ... 25 | def __call__(self, *args: Any, **kwargs: Any) -> _T: ... 26 | 27 | class DecoratorBase(Generic[_T]): 28 | binder_cls: Type[DecoratorBinder[_T]] 29 | def __init__(self, fn: Callable[..., _T]) -> None: ... 30 | @abstractmethod 31 | def name(self) -> str: ... 32 | def is_decorator(self) -> bool: ... 33 | @abstractmethod 34 | def __call__(self, *args: Any, **kwargs: Any) -> _T: ... 35 | def __get__( 36 | self, owner: object, cls: Type[object] 37 | ) -> Union[DecoratorBase[_T], DecoratorBinder[_T]]: ... 38 | 39 | def decorate( 40 | decorator_cls: Type[DecoratorBase[_T]], *args: Any, **kwargs: Any 41 | ) -> Callable[[Callable[..., _T]], Callable[..., _T]]: ... 42 | def deprecated(replacement_description: str) -> Callable[[_CallableT], _CallableT]: ... 43 | 44 | _InputT = TypeVar("_InputT") 45 | _OutputT = TypeVar("_OutputT") 46 | 47 | def convert_result( 48 | converter: Callable[[_InputT], _OutputT], 49 | ) -> Callable[[Callable[..., _InputT]], Callable[..., _OutputT]]: ... 50 | def retry( 51 | exception_cls: Union[Type[BaseException], Tuple[Type[BaseException], ...]], 52 | max_tries: int = ..., 53 | sleep: float = ..., 54 | ) -> Callable[[_CallableT], _CallableT]: ... 55 | def decorator_of_context_manager( 56 | ctxt: Callable[..., ContextManager[Any]], 57 | ) -> Callable[..., Callable[[_CallableT], _CallableT]]: ... 58 | -------------------------------------------------------------------------------- /qcore/disallow_inheritance.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | 17 | Provides a metaclass that prevents inheritance from its instances. 18 | 19 | """ 20 | 21 | 22 | class DisallowInheritance(type): 23 | """Metaclass that disallows inheritance from classes using it.""" 24 | 25 | def __init__(self, cl_name, bases, namespace): 26 | for cls in bases: 27 | if isinstance(cls, DisallowInheritance): 28 | message = ( 29 | "Class %s cannot be used as a base for newly defined class %s" 30 | % (cls, cl_name) 31 | ) 32 | raise TypeError(message) 33 | super().__init__(cl_name, bases, namespace) 34 | 35 | # Needed bcz of a six bug: https://github.com/benjaminp/six/issues/252 36 | @classmethod 37 | def __prepare__(cls, name, bases, **kwargs): 38 | return {} 39 | -------------------------------------------------------------------------------- /qcore/disallow_inheritance.pyi: -------------------------------------------------------------------------------- 1 | class DisallowInheritance(type): ... 2 | -------------------------------------------------------------------------------- /qcore/enum.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | 17 | Enum implementation. 18 | 19 | """ 20 | 21 | __all__ = ["Enum", "EnumType", "EnumValueGenerator", "Flags", "IntEnum"] 22 | 23 | import inspect 24 | import sys 25 | 26 | from . import helpers 27 | from . import inspection 28 | 29 | _no_default = helpers.MarkerObject("no_default @ enums") 30 | 31 | 32 | class EnumType(type): 33 | """Metaclass for all enum types.""" 34 | 35 | def __init__(cls, what, bases=None, dict=None): 36 | super().__init__(what, bases, dict) 37 | cls.process() 38 | 39 | def __contains__(self, k): 40 | return k in self._value_to_name 41 | 42 | def __len__(self): 43 | return len(self._members) 44 | 45 | def __iter__(self): 46 | return iter(self._members) 47 | 48 | def __call__(self, value, default=_no_default): 49 | """Instantiating an Enum always produces an existing value or throws an exception.""" 50 | return self.parse(value, default=default) 51 | 52 | def process(self): 53 | name_to_member = {} 54 | value_to_member = {} 55 | value_to_name = {} 56 | flag_values = [] 57 | members = [] 58 | for k, v in list(inspect.getmembers(self)): 59 | # ensure that names are unicode, even in py2 60 | if isinstance(k, bytes): 61 | k = k.decode("ascii") 62 | if isinstance(type(v), EnumType): 63 | v = v.value # For inherited members 64 | # Ignore private names 65 | if k.startswith("__") and k.endswith("__"): 66 | continue 67 | if isinstance(v, int): 68 | assert ( 69 | v not in value_to_member 70 | ), "Duplicate enum value: %s (class: %s)." % ( 71 | v, 72 | inspection.get_full_name(self), 73 | ) 74 | member = self._make_value(v) 75 | 76 | name_to_member[k] = member 77 | value_to_member[v] = member 78 | value_to_name[v] = k 79 | if v != 0: 80 | flag_values.append(v) 81 | 82 | members.append(member) 83 | self._name_to_member = name_to_member 84 | self._value_to_member = value_to_member 85 | self._value_to_name = value_to_name 86 | self._flag_values = list(reversed(sorted(flag_values))) 87 | self._members = sorted(members, key=lambda m: m.value) 88 | for m in members: 89 | setattr(self, m.short_name, m) 90 | 91 | def _make_value(self, value): 92 | """Instantiates an enum with an arbitrary value.""" 93 | member = self.__new__(self, value) 94 | member.__init__(value) 95 | return member 96 | 97 | # Needed bcz of a six bug: https://github.com/benjaminp/six/issues/252 98 | @classmethod 99 | def __prepare__(cls, name, bases, **kwargs): 100 | return {} 101 | 102 | 103 | class EnumBase(metaclass=EnumType): 104 | _name_to_member = {} 105 | _value_to_member = {} 106 | _value_to_name = {} 107 | _flag_values = [] 108 | _members = [] 109 | 110 | def __init__(self, value): 111 | self.value = int(value) 112 | 113 | @property 114 | def short_name(self): 115 | """Returns the enum member's name, like "foo".""" 116 | raise NotImplementedError 117 | 118 | @property 119 | def long_name(self): 120 | """Returns the enum member's name including the class name, like "MyEnum.foo".""" 121 | return "%s.%s" % (self.__class__.__name__, self.short_name) 122 | 123 | @property 124 | def title(self): 125 | """Returns the enum member's name in title case, like "FooBar" for MyEnum.foo_bar.""" 126 | return self.short_name.replace("_", " ").title() 127 | 128 | @property 129 | def full_name(self): 130 | """Returns the enum meber's name including the module, like "mymodule.MyEnum.foo".""" 131 | return "%s.%s" % (self.__class__.__module__, self.long_name) 132 | 133 | def is_valid(self): 134 | raise NotImplementedError 135 | 136 | def assert_valid(self): 137 | if not self.is_valid(): 138 | raise _create_invalid_value_error(self.__class__, self.value) 139 | 140 | def __int__(self): 141 | return self.value 142 | 143 | def __call__(self): 144 | return self.value 145 | 146 | def __eq__(self, other): 147 | return self.value == other 148 | 149 | def __ne__(self, other): 150 | return self.value != other 151 | 152 | def __hash__(self): 153 | return hash(self.value) 154 | 155 | def __str__(self): 156 | if self.is_valid(): 157 | return self.short_name 158 | else: 159 | return "%s(%s)" % (self.__class__.__name__, self.value) 160 | 161 | def __repr__(self): 162 | if self.is_valid(): 163 | return self.__class__.__name__ + "." + self.short_name 164 | else: 165 | return "%s(%s)" % (self.__class__.__name__, self.value) 166 | 167 | @classmethod 168 | def get_names(cls): 169 | """Returns the names of all members of this enum.""" 170 | return [m.short_name for m in cls._members] 171 | 172 | @classmethod 173 | def get_members(cls): 174 | return cls._members 175 | 176 | @classmethod 177 | def create(cls, name, members): 178 | """Creates a new enum type based on this one (cls) and adds newly 179 | passed members to the newly created subclass of cls. 180 | 181 | This method helps to create enums having the same member values as 182 | values of other enum(s). 183 | 184 | :param name: name of the newly created type 185 | :param members: 1) a dict or 2) a list of (name, value) tuples 186 | and/or EnumBase instances describing new members 187 | :return: newly created enum type. 188 | 189 | """ 190 | NewEnum = type(name, (cls,), {}) 191 | 192 | if isinstance(members, dict): 193 | members = members.items() 194 | for member in members: 195 | if isinstance(member, tuple): 196 | name, value = member 197 | setattr(NewEnum, name, value) 198 | elif isinstance(member, EnumBase): 199 | setattr(NewEnum, member.short_name, member.value) 200 | else: 201 | assert False, ( 202 | "members must be either a dict, " 203 | + "a list of (name, value) tuples, " 204 | + "or a list of EnumBase instances." 205 | ) 206 | 207 | NewEnum.process() 208 | 209 | # needed for pickling to work (hopefully); taken from the namedtuple implementation in the 210 | # standard library 211 | try: 212 | NewEnum.__module__ = sys._getframe(1).f_globals.get("__name__", "__main__") 213 | except (AttributeError, ValueError): 214 | pass 215 | 216 | return NewEnum 217 | 218 | @classmethod 219 | def parse(cls, value, default=_no_default): 220 | """Parses a value into a member of this enum.""" 221 | raise NotImplementedError 222 | 223 | def __reduce_ex__(self, proto): 224 | return self.__class__, (self.value,) 225 | 226 | 227 | class Enum(EnumBase): 228 | def is_valid(self): 229 | return self.value in self._value_to_member 230 | 231 | @property 232 | def short_name(self): 233 | self.assert_valid() 234 | return self._value_to_name[self.value] 235 | 236 | @classmethod 237 | def parse(cls, value, default=_no_default): 238 | """Parses an enum member name or value into an enum member. 239 | 240 | Accepts the following types: 241 | - Members of this enum class. These are returned directly. 242 | - Integers. If there is an enum member with the integer as a value, that member is returned. 243 | - Strings. If there is an enum member with the string as its name, that member is returned. 244 | For integers and strings that don't correspond to an enum member, default is returned; if 245 | no default is given the function raises KeyError instead. 246 | 247 | Examples: 248 | 249 | >>> class Color(Enum): 250 | ... red = 1 251 | ... blue = 2 252 | >>> Color.parse(Color.red) 253 | Color.red 254 | >>> Color.parse(1) 255 | Color.red 256 | >>> Color.parse('blue') 257 | Color.blue 258 | 259 | """ 260 | if isinstance(value, cls): 261 | return value 262 | elif isinstance(value, int) and not isinstance(value, EnumBase): 263 | e = cls._value_to_member.get(value, _no_default) 264 | else: 265 | e = cls._name_to_member.get(value, _no_default) 266 | if e is _no_default or not e.is_valid(): 267 | if default is _no_default: 268 | raise _create_invalid_value_error(cls, value) 269 | return default 270 | return e 271 | 272 | 273 | class Flags(EnumBase): 274 | def is_valid(self): 275 | value = self.value 276 | for v in self._flag_values: 277 | if (v | value) == value: 278 | value ^= v 279 | return value == 0 280 | 281 | @property 282 | def short_name(self): 283 | self.assert_valid() 284 | result = [] 285 | l = self.value 286 | for v in self._flag_values: 287 | if (v | l) == l: 288 | l ^= v 289 | result.append(self._value_to_name[v]) 290 | if not result: 291 | if 0 in self._value_to_name: 292 | return self._value_to_name[0] 293 | else: 294 | return "" 295 | return ",".join(result) 296 | 297 | @classmethod 298 | def parse(cls, value, default=_no_default): 299 | """Parses a flag integer or string into a Flags instance. 300 | 301 | Accepts the following types: 302 | - Members of this enum class. These are returned directly. 303 | - Integers. These are converted directly into a Flags instance with the given name. 304 | - Strings. The function accepts a comma-delimited list of flag names, corresponding to 305 | members of the enum. These are all ORed together. 306 | 307 | Examples: 308 | 309 | >>> class Car(Flags): 310 | ... is_big = 1 311 | ... has_wheels = 2 312 | >>> Car.parse(1) 313 | Car.is_big 314 | >>> Car.parse(3) 315 | Car.parse('has_wheels,is_big') 316 | >>> Car.parse('is_big,has_wheels') 317 | Car.parse('has_wheels,is_big') 318 | 319 | """ 320 | if isinstance(value, cls): 321 | return value 322 | elif isinstance(value, int): 323 | e = cls._make_value(value) 324 | else: 325 | if not value: 326 | e = cls._make_value(0) 327 | else: 328 | r = 0 329 | for k in value.split(","): 330 | v = cls._name_to_member.get(k, _no_default) 331 | if v is _no_default: 332 | if default is _no_default: 333 | raise _create_invalid_value_error(cls, value) 334 | else: 335 | return default 336 | r |= v.value 337 | e = cls._make_value(r) 338 | if not e.is_valid(): 339 | if default is _no_default: 340 | raise _create_invalid_value_error(cls, value) 341 | return default 342 | return e 343 | 344 | def __contains__(self, item): 345 | item = int(item) 346 | if item == 0: 347 | return True 348 | return item == (self.value & item) 349 | 350 | def __or__(self, other): 351 | return self.__class__(self.value | int(other)) 352 | 353 | def __and__(self, other): 354 | return self.__class__(self.value & int(other)) 355 | 356 | def __xor__(self, other): 357 | return self.__class__(self.value ^ int(other)) 358 | 359 | def __repr__(self): 360 | if self.is_valid(): 361 | name = self.short_name 362 | if "," in name: 363 | return "%s.parse('%s')" % (self.__class__.__name__, self.short_name) 364 | else: 365 | return self.__class__.__name__ + "." + self.short_name 366 | else: 367 | return "%s(%s)" % (self.__class__.__name__, self.value) 368 | 369 | 370 | class IntEnum(int, Enum): 371 | """Enum subclass that offers more compatibility with int.""" 372 | 373 | def __repr__(self): 374 | return Enum.__repr__(self) 375 | 376 | 377 | class EnumValueGenerator: 378 | def __init__(self, start=1): 379 | self._next_value = start 380 | 381 | def reset(self, start=1): 382 | self._next_value = start 383 | 384 | def next(self): 385 | result = self._next_value 386 | self._next_value += 1 387 | return result 388 | 389 | def __call__(self): 390 | return self.next() 391 | 392 | def __repr__(self): 393 | return "%s(%r)" % (self.__class__.__name__, self._next_value) 394 | 395 | 396 | # Private part 397 | 398 | 399 | def _create_invalid_value_error(cls, value): 400 | return KeyError("Invalid %s value: %r" % (inspection.get_full_name(cls), value)) 401 | -------------------------------------------------------------------------------- /qcore/enum.pyi: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | Dict, 3 | Iterable, 4 | Iterator, 5 | List, 6 | SupportsInt, 7 | Tuple, 8 | Type, 9 | TypeVar, 10 | Union, 11 | ) 12 | 13 | _T = TypeVar("_T", bound=EnumBase) 14 | 15 | class EnumType(type): 16 | def __contains__(self, k: int) -> bool: ... 17 | def __len__(self) -> int: ... 18 | def __iter__(self) -> Iterator[EnumBase]: ... 19 | def __call__( # type: ignore 20 | self, value: str, default: EnumBase = ... 21 | ) -> EnumBase: ... 22 | def process(self) -> None: ... 23 | 24 | class EnumBase(metaclass=EnumType): 25 | def __init__(self, value: object) -> None: ... 26 | @property 27 | def short_name(self) -> str: ... 28 | @property 29 | def long_name(self) -> str: ... 30 | @property 31 | def title(self) -> str: ... 32 | @property 33 | def full_name(self) -> str: ... 34 | def is_valid(self) -> bool: ... 35 | def assert_valid(self) -> None: ... 36 | def __int__(self) -> int: ... 37 | def __call__(self) -> int: ... 38 | @classmethod 39 | def get_names(cls) -> List[str]: ... 40 | @classmethod 41 | def get_members(cls: Type[_T]) -> List[_T]: ... 42 | @classmethod 43 | def create( 44 | cls: Type[_T], 45 | name: str, 46 | members: Union[Dict[str, int], Iterable[Tuple[str, int]], Iterable[EnumBase]], 47 | ) -> Type[_T]: ... 48 | @classmethod 49 | def parse(cls: Type[_T], value: object, default: object = ...) -> _T: ... 50 | 51 | class Enum(EnumBase): 52 | def is_valid(self) -> bool: ... 53 | @property 54 | def short_name(self) -> str: ... 55 | @classmethod 56 | def parse(cls: Type[_T], value: object, default: object = ...) -> _T: ... 57 | 58 | _FlagsT = TypeVar("_FlagsT", bound=Flags) 59 | 60 | class Flags(EnumBase): 61 | def is_valid(self) -> bool: ... 62 | @property 63 | def short_name(self) -> str: ... 64 | @classmethod 65 | def parse(cls: Type[_T], value: object, default: object = ...) -> _T: ... 66 | def __contains__(self, item: SupportsInt) -> bool: ... 67 | def __or__(self: _FlagsT, other: SupportsInt) -> _FlagsT: ... 68 | def __and__(self: _FlagsT, other: SupportsInt) -> _FlagsT: ... 69 | def __xor__(self: _FlagsT, other: SupportsInt) -> _FlagsT: ... 70 | 71 | class IntEnum(int, Enum): ... 72 | 73 | class EnumValueGenerator: 74 | def __init__(self, start: int = ...) -> None: ... 75 | def reset(self, start: int = ...) -> None: ... 76 | def next(self) -> int: ... 77 | def __call__(self) -> int: ... 78 | -------------------------------------------------------------------------------- /qcore/errors.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | 17 | Common exception types. 18 | 19 | """ 20 | 21 | __all__ = [ 22 | "ArgumentError", 23 | "OperationError", 24 | "NotSupportedError", 25 | "SecurityError", 26 | "PermissionError", 27 | "TimeoutError", 28 | "prepare_for_reraise", 29 | "reraise", 30 | ] 31 | 32 | import sys 33 | 34 | 35 | class ArgumentError(RuntimeError): 36 | """An error related to one of the provided arguments.""" 37 | 38 | pass 39 | 40 | 41 | class OperationError(RuntimeError): 42 | """Indicates the impossibility to perform the action.""" 43 | 44 | pass 45 | 46 | 47 | class NotSupportedError(OperationError): 48 | """An attempt to use an unsupported feature.""" 49 | 50 | pass 51 | 52 | 53 | class SecurityError(OperationError): 54 | """The action can't be performed due to security restrictions.""" 55 | 56 | pass 57 | 58 | 59 | class PermissionError(SecurityError): 60 | """The action can't be performed because of lack of required permissions.""" 61 | 62 | pass 63 | 64 | 65 | class TimeoutError(RuntimeError): 66 | """An error indicating that function was interrupted because of timeout.""" 67 | 68 | pass 69 | 70 | 71 | def prepare_for_reraise(error, exc_info=None): 72 | """Prepares the exception for re-raising with reraise method. 73 | 74 | This method attaches type and traceback info to the error object 75 | so that reraise can properly reraise it using this info. 76 | 77 | """ 78 | if not hasattr(error, "_type_"): 79 | if exc_info is None: 80 | exc_info = sys.exc_info() 81 | error._type_ = exc_info[0] 82 | error._traceback = exc_info[2] 83 | return error 84 | 85 | 86 | __traceback_hide__ = True 87 | 88 | 89 | def reraise(error): 90 | """Re-raises the error that was processed by prepare_for_reraise earlier.""" 91 | if hasattr(error, "_type_"): 92 | raise error.with_traceback(error._traceback) 93 | raise error 94 | -------------------------------------------------------------------------------- /qcore/errors.pyi: -------------------------------------------------------------------------------- 1 | from types import TracebackType 2 | from typing import Optional, Tuple, Type, TypeVar, NoReturn 3 | 4 | class ArgumentError(RuntimeError): ... 5 | class OperationError(RuntimeError): ... 6 | class NotSupportedError(OperationError): ... 7 | class SecurityError(OperationError): ... 8 | class PermissionError(SecurityError): ... 9 | class TimeoutError(RuntimeError): ... 10 | 11 | _ExceptionT = TypeVar("_ExceptionT", bound=BaseException) 12 | 13 | def prepare_for_reraise( 14 | error: _ExceptionT, 15 | exc_info: Optional[Tuple[Type[BaseException], BaseException, TracebackType]] = ..., 16 | ) -> _ExceptionT: ... 17 | def reraise(error: BaseException) -> NoReturn: ... 18 | -------------------------------------------------------------------------------- /qcore/events.pxd: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | cdef class EventHook: 16 | cdef list handlers 17 | 18 | 19 | cdef class SinkingEventHook(EventHook): 20 | pass 21 | 22 | cdef SinkingEventHook sinking_event_hook 23 | 24 | 25 | cdef class EventInterceptor: 26 | cdef object source 27 | cdef dict events 28 | -------------------------------------------------------------------------------- /qcore/events.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | 17 | Event pub/sub routines. 18 | 19 | """ 20 | 21 | import inspect 22 | 23 | from .enum import EnumType, EnumBase 24 | from .errors import prepare_for_reraise, reraise 25 | 26 | 27 | class EventHook: 28 | """This type allows to implement event pattern. 29 | 30 | Allowed operations on EventHook objects: 31 | 32 | * hook.subscribe(handler) # subscribe 33 | * hook.unsubscribe(handler) # unsubscribe (requires O(handlerCount)!) 34 | * hook(...) # invokes all event handlers 35 | * hook.trigger(...) # another way to raise the event 36 | * hook.safe_trigger(...) # definitely invokes all event handlers and raises 37 | # the first thrown exception (if any) 38 | 39 | """ 40 | 41 | def __init__(self, handlers=None): 42 | """Constructor.""" 43 | self.handlers = handlers if handlers is not None else [] 44 | 45 | def subscribe(self, handler): 46 | """Adds a new event handler.""" 47 | assert callable(handler), "Invalid handler %s" % handler 48 | self.handlers.append(handler) 49 | 50 | def unsubscribe(self, handler): 51 | """Removes an event handler.""" 52 | self.handlers.remove(handler) 53 | 54 | def safe_trigger(self, *args): 55 | """*Safely* triggers the event by invoking all its 56 | handlers, even if few of them raise an exception. 57 | 58 | If a set of exceptions is raised during handler 59 | invocation sequence, this method rethrows the first one. 60 | 61 | :param args: the arguments to invoke event handlers with. 62 | 63 | """ 64 | error = None 65 | # iterate over a copy of the original list because some event handlers 66 | # may mutate the list 67 | for handler in list(self.handlers): 68 | try: 69 | handler(*args) 70 | except BaseException as e: 71 | if error is None: 72 | prepare_for_reraise(e) 73 | error = e 74 | if error is not None: 75 | reraise(error) 76 | 77 | def trigger(self, *args): 78 | """Triggers the event by invoking all its handlers 79 | with provided arguments. 80 | 81 | .. note:: 82 | If one of event handlers raises an exception, 83 | other handlers won't be invoked by this method. 84 | 85 | :param args: the arguments to invoke event handlers with. 86 | 87 | """ 88 | for handler in list(self.handlers): 89 | handler(*args) 90 | 91 | def __call__(self, *args): 92 | """A shortcut to trigger method. 93 | 94 | .. note:: 95 | If one of event handlers raises an exception, 96 | other handlers won't be invoked by this method. 97 | 98 | :param args: the arguments to invoke event handlers with. 99 | 100 | """ 101 | self.trigger(*args) 102 | 103 | def __contains__(self, item): 104 | """Checks whether this set contains the specified event handler.""" 105 | return item in self.handlers 106 | 107 | def __iter__(self): 108 | """Iterates through all registered event handlers.""" 109 | for handler in self.handlers: 110 | yield handler 111 | 112 | def __str__(self): 113 | """Gets the string representation of this object.""" 114 | return "EventHook" + repr(tuple(self.handlers)) 115 | 116 | def __repr__(self): 117 | """Gets the ``repr`` representation of this object.""" 118 | return self.__str__() 119 | 120 | 121 | class SinkingEventHook(EventHook): 122 | """An implementation of EventHook that actually does nothing. 123 | This type allows to implement a simple performance 124 | optimization for ConstFuture, ErrorFuture and similar 125 | classes, since they never raise their events. 126 | 127 | """ 128 | 129 | def subscribe(self, handler): 130 | """Does nothing.""" 131 | return self 132 | 133 | def unsubscribe(self, handler): 134 | """Does nothing.""" 135 | return self 136 | 137 | def safe_trigger(self, *args): 138 | """Does nothing.""" 139 | return 140 | 141 | def trigger(self, *args): 142 | """Does nothing.""" 143 | return 144 | 145 | def __call__(self, *args): 146 | """Does nothing.""" 147 | return 148 | 149 | def __contains__(self, item): 150 | """Always returns False.""" 151 | return False 152 | 153 | def __iter__(self): 154 | """Returns empty generator.""" 155 | return iter([]) 156 | 157 | def __str__(self): 158 | """Gets the string representation of this object.""" 159 | return "SinkingEventHook()" 160 | 161 | 162 | sinking_event_hook = SinkingEventHook() 163 | globals()["sinking_event_hook"] = sinking_event_hook 164 | 165 | 166 | class EventInterceptor: 167 | """A context object helping to temporarily intercept 168 | a set of events on an object exposing a set of event hooks. 169 | 170 | """ 171 | 172 | def __init__(self, source, **events): 173 | """ 174 | Constructor. 175 | 176 | :param source: the object exposing a set of event hook properies 177 | :param events: a set of event_hook_name=event_handler pairs specifying 178 | which events to intercept. 179 | """ 180 | self.source = source 181 | self.events = events 182 | 183 | def __enter__(self): 184 | """Starts event interception.""" 185 | source = self.source 186 | for name, handler in self.events.items(): 187 | hook = getattr(source, name) 188 | hook.subscribe(handler) 189 | 190 | def __exit__(self, typ, value, traceback): 191 | """Stops event interception.""" 192 | source = self.source 193 | for name, handler in self.events.items(): 194 | hook = getattr(source, name) 195 | hook.unsubscribe(handler) 196 | 197 | 198 | class EventHub: 199 | """Provides named event hooks on demand. 200 | 201 | Use properties (or keys) of this object to access 202 | named event hooks created on demand (i.e. on the first 203 | access attempt). 204 | 205 | """ 206 | 207 | def __init__(self, source=None): 208 | """Constructor. 209 | 210 | :param source: ``dict`` with initial set of named event hooks. 211 | 212 | """ 213 | if source is not None: 214 | self.__dict__ = source 215 | 216 | def on(self, event, handler): 217 | """Attaches the handler to the specified event. 218 | 219 | @param event: event to attach the handler to. Any object can be passed 220 | as event, but string is preferable. If qcore.EnumBase 221 | instance is passed, its name is used as event key. 222 | @param handler: event handler. 223 | @return: self, so calls like this can be chained together. 224 | 225 | """ 226 | event_hook = self.get_or_create(event) 227 | event_hook.subscribe(handler) 228 | return self 229 | 230 | def off(self, event, handler): 231 | """Detaches the handler from the specified event. 232 | 233 | @param event: event to detach the handler to. Any object can be passed 234 | as event, but string is preferable. If qcore.EnumBase 235 | instance is passed, its name is used as event key. 236 | @param handler: event handler. 237 | @return: self, so calls like this can be chained together. 238 | 239 | """ 240 | event_hook = self.get_or_create(event) 241 | event_hook.unsubscribe(handler) 242 | return self 243 | 244 | def trigger(self, event, *args): 245 | """Triggers the specified event by invoking EventHook.trigger under the hood. 246 | 247 | @param event: event to trigger. Any object can be passed 248 | as event, but string is preferable. If qcore.EnumBase 249 | instance is passed, its name is used as event key. 250 | @param args: event arguments. 251 | @return: self, so calls like this can be chained together. 252 | 253 | """ 254 | event_hook = self.get_or_create(event) 255 | event_hook.trigger(*args) 256 | return self 257 | 258 | def safe_trigger(self, event, *args): 259 | """Safely triggers the specified event by invoking 260 | EventHook.safe_trigger under the hood. 261 | 262 | @param event: event to trigger. Any object can be passed 263 | as event, but string is preferable. If qcore.EnumBase 264 | instance is passed, its name is used as event key. 265 | @param args: event arguments. 266 | @return: self, so calls like this can be chained together. 267 | 268 | """ 269 | event_hook = self.get_or_create(event) 270 | event_hook.safe_trigger(*args) 271 | return self 272 | 273 | def get_or_create(self, event): 274 | """Gets or creates a new event hook for the specified event (key). 275 | 276 | This method treats qcore.EnumBase-typed event keys specially: 277 | enum_member.name is used as key instead of enum instance 278 | in case such a key is passed. 279 | 280 | Note that on/off/trigger/safe_trigger methods rely on this method, 281 | so you can pass enum members there as well. 282 | 283 | """ 284 | if isinstance(event, EnumBase): 285 | event = event.short_name 286 | return self.__dict__.setdefault(event, EventHook()) 287 | 288 | def __getattr__(self, key): 289 | """Gets or creates a new event hook with the specified name. 290 | Calls get_or_create under the hood. 291 | 292 | Specified key must start with ``on_`` prefix; this prefix is 293 | trimmed when key is passed to self.get_or_create. 294 | 295 | """ 296 | if key.startswith("on_"): 297 | return self.get_or_create(key[3:]) 298 | else: 299 | raise AttributeError(key) 300 | 301 | def __contains__(self, item): 302 | """Checks if there is an event hook with the specified name.""" 303 | return item in self.__dict__ 304 | 305 | def __len__(self): 306 | """Gets the count of created event hooks.""" 307 | return len(self.__dict__) 308 | 309 | def __getitem__(self, item): 310 | """Gets the event hook with the specified name.""" 311 | return self.__dict__[item] 312 | 313 | def __setitem__(self, key, value): 314 | """Sets the event hook by its name.""" 315 | self.__dict__[key] = value 316 | 317 | def __delitem__(self, key): 318 | """Removes the event hook with the specified name.""" 319 | del self.__dict__[key] 320 | 321 | def __iter__(self): 322 | """Iterates over all (name, event_hook) pairs.""" 323 | return iter(self.__dict__.items()) 324 | 325 | def __repr__(self): 326 | """Gets the ``repr`` representation of this object.""" 327 | return "%s(%r)" % (self.__class__.__name__, self.__dict__) 328 | 329 | # Needed bcz of a six bug: https://github.com/benjaminp/six/issues/252 330 | @classmethod 331 | def __prepare__(cls, name, bases, **kwargs): 332 | return {} 333 | 334 | 335 | class EnumBasedEventHubType(type): 336 | """Metaclass for enum-based event hubs. 337 | 338 | Asserts that all enum members are defined in class and vice versa. 339 | 340 | """ 341 | 342 | def __init__(cls, what, bases=None, dict=None): 343 | super().__init__(what, bases, dict) 344 | if cls.__name__ == "NewBase" and cls.__module__ == "six" and not dict: 345 | # some versions of six generate an intermediate class that is created without a 346 | # __based_on__ 347 | return 348 | assert dict is not None and "__based_on__" in dict, ( 349 | "__based_on__ = [EnumA, EnumB] class member " 350 | "must be used to subclass EnumBasedEventHub" 351 | ) 352 | based_on = cls.__based_on__ 353 | if isinstance(based_on, EnumType): 354 | based_on = [based_on] 355 | 356 | cls_member_names = set() 357 | for k, v in inspect.getmembers(cls): 358 | if not k.startswith("on_"): 359 | continue 360 | if not isinstance(v, EventHook): 361 | continue 362 | cls_member_names.add(k[3:]) 363 | 364 | enum_members = {} 365 | for enum_type in based_on: 366 | for member in enum_type.get_members(): 367 | name = member.short_name 368 | assert ( 369 | name not in enum_members 370 | ), "Two enum members share the same name: %r and %r " % ( 371 | member, 372 | enum_members[name], 373 | ) 374 | enum_members[name] = member 375 | enum_member_names = set(enum_members.keys()) 376 | 377 | for name in enum_member_names: 378 | assert name in cls_member_names, ( 379 | "Member %r is declared in one of enums, " 380 | + "but %r is not declared in class." 381 | ) % (name, "on_" + name) 382 | for name in cls_member_names: 383 | assert name in enum_member_names, ( 384 | "Member %r is declared in class, " 385 | + "but %r is not declared in any of enum(s)." 386 | ) % ("on_" + name, name) 387 | # Members are removed from class, since EventHub anyway creates 388 | # similar instance members 389 | delattr(cls, "on_" + name) 390 | 391 | # Needed bcz of a six bug: https://github.com/benjaminp/six/issues/252 392 | @classmethod 393 | def __prepare__(cls, name, bases, **kwargs): 394 | return {} 395 | 396 | 397 | class EnumBasedEventHub(EventHub, metaclass=EnumBasedEventHubType): 398 | __based_on__ = [] 399 | 400 | 401 | hub = EventHub() # Default global event hub 402 | -------------------------------------------------------------------------------- /qcore/events.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, TypeVar 2 | from types import TracebackType 3 | 4 | _HandlerT = Callable[..., Any] 5 | 6 | class EventHook: 7 | def __init__(self, handlers: Optional[List[_HandlerT]] = ...) -> None: ... 8 | def subscribe(self, handler: _HandlerT) -> None: ... 9 | def unsubscribe(self, handler: _HandlerT) -> None: ... 10 | def safe_trigger(self, *args: Any) -> None: ... 11 | def trigger(self, *args: Any) -> None: ... 12 | def __call__(self, *args: Any) -> None: ... 13 | def __contains__(self, item: _HandlerT) -> bool: ... 14 | def __iter__(self) -> Iterator[_HandlerT]: ... 15 | 16 | class SinkingEventHook(EventHook): ... 17 | 18 | sinking_event_hook: SinkingEventHook 19 | 20 | class EventInterceptor: 21 | source: object 22 | events: Dict[str, _HandlerT] 23 | def __init__(self, source: object, **events: _HandlerT) -> None: ... 24 | def __enter__(self) -> None: ... 25 | def __exit__( 26 | self, 27 | typ: Optional[Type[BaseException]], 28 | value: Optional[BaseException], 29 | traceback: Optional[TracebackType], 30 | ) -> None: ... 31 | 32 | _HubT = TypeVar("_HubT", bound=EventHub) 33 | 34 | class EventHub: 35 | def __init__(self, source: Optional[Dict[Any, Any]] = ...) -> None: ... 36 | def on(self: _HubT, event: object, handler: _HandlerT) -> _HubT: ... 37 | def off(self: _HubT, event: object, handler: _HandlerT) -> _HubT: ... 38 | def trigger(self: _HubT, event: object, *args: Any) -> _HubT: ... 39 | def safe_trigger(self: _HubT, event: object, *args: Any) -> _HubT: ... 40 | def get_or_create(self, event: object) -> EventHook: ... 41 | def __getattr__(self, key: str) -> EventHook: ... 42 | def __contains__(self, item: str) -> bool: ... 43 | def __len__(self) -> int: ... 44 | def __getitem__(self, item: str) -> EventHook: ... 45 | def __setitem__(self, key: str, value: EventHook) -> None: ... 46 | def __delitem__(self, key: str) -> None: ... 47 | def __iter__(self) -> Iterator[Tuple[str, EventHook]]: ... 48 | 49 | class EnumBasedEventHubType(type): ... 50 | class EnumBasedEventHub(EventHub, metaclass=EnumBasedEventHubType): ... 51 | 52 | hub: EventHub 53 | -------------------------------------------------------------------------------- /qcore/helpers.pxd: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import cython 16 | from cpython cimport bool 17 | 18 | 19 | cdef list empty_list 20 | cdef tuple empty_tuple 21 | cdef dict empty_dict 22 | 23 | 24 | cdef class MarkerObject: 25 | cdef unicode name 26 | 27 | cdef MarkerObject none 28 | cdef MarkerObject miss 29 | cdef MarkerObject same 30 | cdef MarkerObject unspecified 31 | 32 | 33 | cdef class EmptyContext: 34 | cpdef __enter__(self) 35 | cpdef __exit__(self, exc_type, exc_val, exc_tb) 36 | 37 | cdef EmptyContext empty_context 38 | 39 | 40 | cdef class CythonCachedHashWrapper: 41 | cdef object _value 42 | cdef int _hash 43 | 44 | cpdef object value(self) 45 | cpdef object hash(self) 46 | 47 | cdef object CachedHashWrapper 48 | 49 | 50 | cdef class _ScopedValueOverrideContext(object) # Forward declaration 51 | 52 | cdef class ScopedValue: 53 | cdef object _value 54 | 55 | cpdef object get(self) 56 | cpdef set(self, object value) 57 | cpdef object override(self, object value) 58 | 59 | cdef class _ScopedValueOverrideContext: 60 | cdef ScopedValue _target 61 | cdef object _value 62 | cdef object _old_value 63 | 64 | 65 | cdef class _PropertyOverrideContext: 66 | cdef object _target 67 | cdef object _property_name 68 | cdef object _value 69 | cdef object _old_value 70 | 71 | cdef object override # Alias of PropertyOverrideContext 72 | 73 | 74 | cpdef object ellipsis(object source, int max_length) 75 | cpdef object safe_str(object source, int max_length=?) 76 | cpdef object safe_repr(object source, int max_length=?) 77 | 78 | 79 | cpdef dict_to_object(dict source) 80 | -------------------------------------------------------------------------------- /qcore/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | 17 | Various small helper classes and routines. 18 | 19 | """ 20 | 21 | import inspect 22 | 23 | from . import inspectable_class 24 | 25 | # export it here for backward compatibility 26 | from .disallow_inheritance import DisallowInheritance 27 | 28 | 29 | empty_tuple = () 30 | empty_list = [] 31 | empty_dict = {} 32 | globals()["empty_tuple"] = empty_tuple 33 | globals()["empty_list"] = empty_list 34 | globals()["empty_dict"] = empty_dict 35 | 36 | 37 | def true_fn(): 38 | return True 39 | 40 | 41 | def false_fn(): 42 | return False 43 | 44 | 45 | class MarkerObject: 46 | """Replaces None in cases when None value is also expected. 47 | Used mainly by caches to describe a cache miss. 48 | 49 | """ 50 | 51 | def __init__(self, name): 52 | if isinstance(name, bytes): 53 | raise TypeError("name must be str, not bytes") 54 | self.name = name 55 | 56 | def __str__(self): 57 | return self.name 58 | 59 | def __repr__(self): 60 | return self.name 61 | 62 | 63 | none = MarkerObject("none") 64 | miss = MarkerObject("miss") 65 | same = MarkerObject("same") 66 | unspecified = MarkerObject("unspecified") 67 | globals()["none"] = none 68 | globals()["miss"] = miss 69 | globals()["same"] = same 70 | globals()["unspecified"] = unspecified 71 | 72 | 73 | class EmptyContext: 74 | def __enter__(self): 75 | pass 76 | 77 | def __exit__(self, exc_type, exc_val, exc_tb): 78 | pass 79 | 80 | def __repr__(self): 81 | return "qcore.empty_context" 82 | 83 | 84 | empty_context = EmptyContext() 85 | globals()["empty_context"] = empty_context 86 | 87 | 88 | class CythonCachedHashWrapper: 89 | def __init__(self, value): 90 | self._value = value 91 | self._hash = hash(value) 92 | 93 | def value(self): 94 | return self._value 95 | 96 | def hash(self): 97 | return self._hash 98 | 99 | def __call__(self): 100 | return self._value 101 | 102 | def __hash__(self): 103 | return self._hash 104 | 105 | def __richcmp__(self, other, op): 106 | # Cython way of implementing comparison operations 107 | if op == 2: 108 | return ( 109 | self() == other() 110 | if isinstance(other, CachedHashWrapper) 111 | else self() == other 112 | ) 113 | elif op == 3: 114 | return not ( 115 | self() == other() 116 | if isinstance(other, CachedHashWrapper) 117 | else self() == other 118 | ) 119 | else: 120 | raise NotImplementedError("only == and != are supported") 121 | 122 | def __repr__(self): 123 | return "%s(%r)" % (self.__class__.__name__, self._value) 124 | 125 | 126 | CachedHashWrapper = CythonCachedHashWrapper 127 | globals()["CachedHashWrapper"] = CythonCachedHashWrapper 128 | if hasattr(CythonCachedHashWrapper, "__richcmp__"): 129 | # This isn't Cython, so we must add eq and ne to make it work w/o Cython 130 | class PythonCachedHashWrapper(CachedHashWrapper): 131 | def __eq__(self, other): 132 | return ( 133 | self._value == other._value 134 | if isinstance(other, CachedHashWrapper) 135 | else self._value == other 136 | ) 137 | 138 | def __ne__(self, other): 139 | return not ( 140 | self._value == other._value 141 | if isinstance(other, CachedHashWrapper) 142 | else self._value == other 143 | ) 144 | 145 | # needed in Python 3 because this class overrides __eq__ 146 | def __hash__(self): 147 | return self._hash 148 | 149 | CachedHashWrapper = PythonCachedHashWrapper 150 | globals()["CachedHashWrapper"] = PythonCachedHashWrapper 151 | 152 | 153 | class ScopedValue: 154 | def __init__(self, default): 155 | self._value = default 156 | 157 | def get(self): 158 | return self._value 159 | 160 | def set(self, value): 161 | self._value = value 162 | 163 | def override(self, value): 164 | """Temporarily overrides the old value with the new one.""" 165 | if self._value is not value: 166 | return _ScopedValueOverrideContext(self, value) 167 | else: 168 | return empty_context 169 | 170 | def __call__(self): 171 | """Same as get.""" 172 | return self._value 173 | 174 | def __str__(self): 175 | return "ScopedValue(%s)" % (self._value,) 176 | 177 | def __repr__(self): 178 | return "ScopedValue(%r)" % (self._value,) 179 | 180 | 181 | class _ScopedValueOverrideContext: 182 | def __init__(self, target, value): 183 | self._target = target 184 | self._value = value 185 | self._old_value = None 186 | 187 | def __enter__(self): 188 | self._old_value = self._target._value 189 | self._target._value = self._value 190 | 191 | def __exit__(self, exc_type, exc_value, tb): 192 | self._target._value = self._old_value 193 | 194 | 195 | class _PropertyOverrideContext: 196 | def __init__(self, target, property_name, value): 197 | self._target = target 198 | self._property_name = property_name 199 | self._value = value 200 | self._old_value = None 201 | 202 | def __enter__(self): 203 | self._old_value = getattr(self._target, self._property_name) 204 | setattr(self._target, self._property_name, self._value) 205 | 206 | def __exit__(self, exc_type, exc_value, tb): 207 | setattr(self._target, self._property_name, self._old_value) 208 | 209 | 210 | override = _PropertyOverrideContext 211 | globals()["override"] = override 212 | 213 | 214 | def ellipsis(source, max_length): 215 | """Truncates a string to be at most max_length long.""" 216 | if max_length == 0 or len(source) <= max_length: 217 | return source 218 | return source[: max(0, max_length - 3)] + "..." 219 | 220 | 221 | def safe_str(source, max_length=0): 222 | """Wrapper for str() that catches exceptions.""" 223 | try: 224 | return ellipsis(str(source), max_length) 225 | except Exception as e: 226 | return ellipsis("" % e, max_length) 227 | 228 | 229 | def safe_repr(source, max_length=0): 230 | """Wrapper for repr() that catches exceptions.""" 231 | try: 232 | return ellipsis(repr(source), max_length) 233 | except Exception as e: 234 | return ellipsis("" % e, max_length) 235 | 236 | 237 | def dict_to_object(source): 238 | """Returns an object with the key-value pairs in source as attributes.""" 239 | target = inspectable_class.InspectableClass() 240 | for k, v in source.items(): 241 | setattr(target, k, v) 242 | return target 243 | 244 | 245 | def copy_public_attrs(source_obj, dest_obj): 246 | """Shallow copies all public attributes from source_obj to dest_obj. 247 | 248 | Overwrites them if they already exist. 249 | 250 | """ 251 | for name, value in inspect.getmembers(source_obj): 252 | if not any(name.startswith(x) for x in ["_", "func", "im"]): 253 | setattr(dest_obj, name, value) 254 | 255 | 256 | def object_from_string(name): 257 | """Creates a Python class or function from its fully qualified name. 258 | 259 | :param name: A fully qualified name of a class or a function. In Python 3 this 260 | is only allowed to be of text type (unicode). In Python 2, both bytes and unicode 261 | are allowed. 262 | :return: A function or class object. 263 | 264 | This method is used by serialization code to create a function or class 265 | from a fully qualified name. 266 | 267 | """ 268 | if not isinstance(name, str): 269 | raise TypeError("name must be str, not %r" % type(name)) 270 | 271 | pos = name.rfind(".") 272 | if pos < 0: 273 | raise ValueError("Invalid function or class name %s" % name) 274 | module_name = name[:pos] 275 | func_name = name[pos + 1 :] 276 | try: 277 | mod = __import__(module_name, fromlist=[func_name], level=0) 278 | except ImportError: 279 | # Hail mary. if the from import doesn't work, then just import the top level module 280 | # and do getattr on it, one level at a time. This will handle cases where imports are 281 | # done like `from . import submodule as another_name` 282 | parts = name.split(".") 283 | mod = __import__(parts[0], level=0) 284 | for i in range(1, len(parts)): 285 | mod = getattr(mod, parts[i]) 286 | return mod 287 | 288 | else: 289 | return getattr(mod, func_name) 290 | 291 | 292 | def catchable_exceptions(exceptions): 293 | """Returns True if exceptions can be caught in the except clause. 294 | 295 | The exception can be caught if it is an Exception type or a tuple of 296 | exception types. 297 | 298 | """ 299 | if isinstance(exceptions, type) and issubclass(exceptions, BaseException): 300 | return True 301 | 302 | if ( 303 | isinstance(exceptions, tuple) 304 | and exceptions 305 | and all(issubclass(it, BaseException) for it in exceptions) 306 | ): 307 | return True 308 | 309 | return False 310 | -------------------------------------------------------------------------------- /qcore/helpers.pyi: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | Any, 3 | ContextManager, 4 | Dict, 5 | Generic, 6 | List, 7 | Optional, 8 | Text, 9 | Tuple, 10 | Type, 11 | TypeVar, 12 | ) 13 | from types import TracebackType 14 | 15 | from .disallow_inheritance import DisallowInheritance as DisallowInheritance 16 | 17 | _T = TypeVar("_T") 18 | _T_co = TypeVar("_T_co", covariant=True) 19 | 20 | empty_tuple: Tuple[Any, ...] 21 | empty_list: List[Any] 22 | empty_dict: Dict[Any, Any] 23 | 24 | def true_fn() -> bool: ... 25 | def false_fn() -> bool: ... 26 | 27 | class MarkerObject: 28 | name: Text 29 | def __init__(self, name: Text) -> None: ... 30 | 31 | none: MarkerObject 32 | miss: MarkerObject 33 | same: MarkerObject 34 | unspecified: MarkerObject 35 | 36 | class EmptyContext: 37 | def __enter__(self) -> None: ... 38 | def __exit__( 39 | self, 40 | exc_type: Optional[Type[BaseException]], 41 | exc_val: Optional[BaseException], 42 | exc_tb: Optional[TracebackType], 43 | ) -> None: ... 44 | 45 | empty_context: EmptyContext 46 | 47 | class CythonCachedHashWrapper(Generic[_T_co]): 48 | def __init__(self, value: _T_co) -> None: ... 49 | def value(self) -> _T_co: ... 50 | def hash(self) -> int: ... 51 | def __call__(self) -> _T_co: ... 52 | 53 | CachedHashWrapper = CythonCachedHashWrapper 54 | 55 | class ScopedValue(Generic[_T]): 56 | def __init__(self, default: _T) -> None: ... 57 | def get(self) -> _T: ... 58 | def set(self, value: _T) -> None: ... 59 | def override(self, value: _T) -> ContextManager[None]: ... 60 | def __call__(self) -> _T: ... 61 | 62 | class _PropertyOverrideContext: 63 | def __init__(self, target: object, property_name: str, value: object) -> None: ... 64 | def __enter__(self) -> None: ... 65 | def __exit__( 66 | self, 67 | exc_type: Optional[Type[BaseException]], 68 | exc_val: Optional[BaseException], 69 | exc_tb: Optional[TracebackType], 70 | ) -> None: ... 71 | 72 | override = _PropertyOverrideContext 73 | 74 | def ellipsis(source: str, max_length: int) -> str: ... 75 | def safe_str(source: object, max_length: int = ...) -> str: ... 76 | def safe_repr(source: object, max_length: int = ...) -> str: ... 77 | def dict_to_object(source: Dict[str, Any]) -> Any: ... 78 | def copy_public_attrs(source_obj: object, dest_obj: object) -> None: ... 79 | def object_from_string(name: Text) -> Any: ... 80 | def catchable_exceptions(exceptions: object) -> bool: ... 81 | -------------------------------------------------------------------------------- /qcore/inspectable_class.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | 17 | Provides a base class overriding some useful dunder methods. 18 | 19 | """ 20 | 21 | 22 | class InspectableClass: 23 | """Class that provides commonly useful dunder methods. 24 | 25 | This creates a useful repr/str representation, implements equality checking, and provides 26 | hashing. 27 | 28 | """ 29 | 30 | _excluded_attributes = set() # these are not used in equality checking and repr 31 | 32 | def _filtered_dict(self): 33 | excluded = self._excluded_attributes 34 | typ = type(self) 35 | if hasattr(typ, "__slots__"): 36 | items = ((slot, getattr(self, slot)) for slot in typ.__slots__) 37 | else: 38 | items = self.__dict__.items() 39 | if excluded: 40 | items = ((k, v) for k, v in items if k not in excluded) 41 | return sorted(items, key=lambda pair: pair[0]) 42 | 43 | def __repr__(self): 44 | return "%s(%s)" % ( 45 | self.__class__.__name__, 46 | ", ".join("%s=%r" % pair for pair in self._filtered_dict()), 47 | ) 48 | 49 | def __str__(self): 50 | return repr(self) 51 | 52 | def __hash__(self): 53 | return hash(tuple(self._filtered_dict())) 54 | 55 | def __eq__(self, other): 56 | if type(self) is not type(other): 57 | return NotImplemented 58 | return self._filtered_dict() == other._filtered_dict() 59 | 60 | def __ne__(self, other): 61 | if type(self) is not type(other): 62 | return NotImplemented 63 | return self._filtered_dict() != other._filtered_dict() 64 | -------------------------------------------------------------------------------- /qcore/inspectable_class.pyi: -------------------------------------------------------------------------------- 1 | class InspectableClass: ... 2 | -------------------------------------------------------------------------------- /qcore/inspection.pxd: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import cython 16 | 17 | 18 | cpdef object get_original_fn(object fn) 19 | 20 | @cython.locals(_full_name_=str) 21 | cpdef str get_full_name(object src) 22 | 23 | @cython.locals(result=str, first=bint) 24 | cpdef str get_function_call_str(fn, tuple args, dict kwargs) 25 | @cython.locals(result=str, first=bint) 26 | cpdef str get_function_call_repr(fn, tuple args, dict kwargs) 27 | 28 | cpdef object getargspec(object func) 29 | 30 | cpdef bint is_cython_or_generator(object fn) except -1 31 | @cython.locals(name=str) 32 | cpdef bint is_cython_function(object fn) except -1 33 | 34 | cpdef bint is_cython_class(object cls) except -1 35 | 36 | cpdef bint is_classmethod(object fn) except -1 37 | 38 | cpdef wraps(object wrapped, object assigned=?, object updated=?) 39 | -------------------------------------------------------------------------------- /qcore/inspection.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | 17 | Code inspection helpers. 18 | 19 | """ 20 | 21 | from collections import namedtuple 22 | import functools 23 | import inspect 24 | import sys 25 | 26 | 27 | def get_original_fn(fn): 28 | """Gets the very original function of a decorated one.""" 29 | 30 | fn_type = type(fn) 31 | if fn_type is classmethod or fn_type is staticmethod: 32 | return get_original_fn(fn.__func__) 33 | if hasattr(fn, "original_fn"): 34 | return fn.original_fn 35 | if hasattr(fn, "fn"): 36 | original_fn = get_original_fn(fn.fn) 37 | try: 38 | fn.original_fn = original_fn 39 | except AttributeError: 40 | pass 41 | return original_fn 42 | return fn 43 | 44 | 45 | def get_full_name(src): 46 | """Gets full class or function name.""" 47 | 48 | if hasattr(src, "is_decorator"): 49 | # Our own decorator or binder 50 | if hasattr(src, "decorator"): 51 | # Our own binder 52 | return str(src.decorator) 53 | else: 54 | # Our own decorator 55 | return str(src) 56 | elif hasattr(src, "im_class"): 57 | # Bound method 58 | cls = src.im_class 59 | return get_full_name(cls) + "." + src.__name__ 60 | elif hasattr(src, "__module__") and hasattr(src, "__name__"): 61 | # Func or class 62 | return ( 63 | ("" if src.__module__ is None else src.__module__) 64 | + "." 65 | + src.__name__ 66 | ) 67 | else: 68 | # Something else 69 | return str(get_original_fn(src)) 70 | 71 | 72 | def _str_converter(v): 73 | try: 74 | return str(v) 75 | except Exception: 76 | try: 77 | return repr(v) 78 | except Exception: 79 | return "" 80 | 81 | 82 | def get_function_call_str(fn, args, kwargs): 83 | """Converts method call (function and its arguments) to a str(...)-like string.""" 84 | 85 | result = get_full_name(fn) + "(" 86 | first = True 87 | for v in args: 88 | if first: 89 | first = False 90 | else: 91 | result += "," 92 | result += _str_converter(v) 93 | for k, v in kwargs.items(): 94 | if first: 95 | first = False 96 | else: 97 | result += "," 98 | result += str(k) + "=" + _str_converter(v) 99 | result += ")" 100 | return result 101 | 102 | 103 | def get_function_call_repr(fn, args, kwargs): 104 | """Converts method call (function and its arguments) to a repr(...)-like string.""" 105 | 106 | result = get_full_name(fn) + "(" 107 | first = True 108 | for v in args: 109 | if first: 110 | first = False 111 | else: 112 | result += "," 113 | result += repr(v) 114 | for k, v in kwargs.items(): 115 | if first: 116 | first = False 117 | else: 118 | result += "," 119 | result += str(k) + "=" + repr(v) 120 | result += ")" 121 | return result 122 | 123 | 124 | if sys.version_info >= (3, 10): 125 | ArgSpec = namedtuple("ArgSpec", "args varargs keywords defaults") 126 | else: 127 | ArgSpec = inspect.ArgSpec 128 | 129 | 130 | def getargspec(func): 131 | """Variation of inspect.getargspec that works for more functions. 132 | 133 | This function works for Cythonized, non-cpdef functions, which expose argspec information but 134 | are not accepted by getargspec. It also works for Python 3 functions that use annotations, which 135 | are simply ignored. However, keyword-only arguments are not supported. 136 | 137 | """ 138 | if inspect.ismethod(func): 139 | func = func.__func__ 140 | # Cythonized functions have a .__code__, but don't pass inspect.isfunction() 141 | try: 142 | code = func.__code__ 143 | except AttributeError: 144 | raise TypeError("{!r} is not a Python function".format(func)) 145 | if hasattr(code, "co_kwonlyargcount") and code.co_kwonlyargcount > 0: 146 | raise ValueError("keyword-only arguments are not supported by getargspec()") 147 | args, varargs, varkw = inspect.getargs(code) 148 | return ArgSpec(args, varargs, varkw, func.__defaults__) 149 | 150 | 151 | def is_cython_or_generator(fn): 152 | """Returns whether this function is either a generator function or a Cythonized function.""" 153 | if hasattr(fn, "__func__"): 154 | fn = fn.__func__ # Class method, static method 155 | if inspect.isgeneratorfunction(fn): 156 | return True 157 | name = type(fn).__name__ 158 | return ( 159 | name == "generator" 160 | or name == "method_descriptor" 161 | or name == "cython_function_or_method" 162 | or name == "builtin_function_or_method" 163 | ) 164 | 165 | 166 | def is_cython_function(fn): 167 | """Checks if a function is compiled w/Cython.""" 168 | if hasattr(fn, "__func__"): 169 | fn = fn.__func__ # Class method, static method 170 | name = type(fn).__name__ 171 | return ( 172 | name == "method_descriptor" 173 | or name == "cython_function_or_method" 174 | or name == "builtin_function_or_method" 175 | ) 176 | 177 | 178 | def is_cython_class(cls): 179 | """Returns whether a class is a Cython extension class.""" 180 | return "__pyx_vtable__" in cls.__dict__ 181 | 182 | 183 | def is_classmethod(fn): 184 | """Returns whether f is a classmethod.""" 185 | # This is True for bound methods 186 | if not inspect.ismethod(fn): 187 | return False 188 | if not hasattr(fn, "__self__"): 189 | return False 190 | im_self = fn.__self__ 191 | # This is None for instance methods on classes, but True 192 | # for instance methods on instances. 193 | if im_self is None: 194 | return False 195 | return isinstance(im_self, type) 196 | 197 | 198 | def _identity(wrapper): 199 | return wrapper 200 | 201 | 202 | def wraps( 203 | wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, updated=functools.WRAPPER_UPDATES 204 | ): 205 | """Cython-compatible functools.wraps implementation.""" 206 | if not is_cython_function(wrapped): 207 | return functools.wraps(wrapped, assigned, updated) 208 | else: 209 | return _identity 210 | 211 | 212 | def get_subclass_tree(cls, ensure_unique=True): 213 | """Returns all subclasses (direct and recursive) of cls.""" 214 | subclasses = [] 215 | # cls.__subclasses__() fails on classes inheriting from type 216 | for subcls in type.__subclasses__(cls): 217 | subclasses.append(subcls) 218 | subclasses.extend(get_subclass_tree(subcls, ensure_unique)) 219 | return list(set(subclasses)) if ensure_unique else subclasses 220 | 221 | 222 | def lazy_stack(): 223 | """Return a generator of records for the stack above the caller's frame. 224 | 225 | Equivalent to inspect.stack() but potentially faster because it does not compute info for all 226 | stack frames. 227 | 228 | As a further optimization, yields raw frame objects instead of tuples describing the frame. To 229 | get the full information, call inspect.getframeinfo(frame). 230 | 231 | """ 232 | frame = sys._getframe(1) 233 | 234 | while frame: 235 | yield frame 236 | frame = frame.f_back 237 | -------------------------------------------------------------------------------- /qcore/inspection.pyi: -------------------------------------------------------------------------------- 1 | import inspect 2 | import types 3 | from typing import ( 4 | Any, 5 | Callable, 6 | Iterable, 7 | List, 8 | Mapping, 9 | Sequence, 10 | Type, 11 | TypeVar, 12 | Union, 13 | ) 14 | 15 | _AnyCallable = TypeVar("_AnyCallable", bound=Callable[..., Any]) 16 | _T = TypeVar("_T") 17 | 18 | def get_original_fn(fn: Callable[..., Any]) -> Callable[..., Any]: ... 19 | def get_full_name(src: object) -> str: ... 20 | def get_function_call_str( 21 | fn: Callable[..., Any], args: Iterable[object], kwargs: Mapping[str, object] 22 | ) -> str: ... 23 | def get_function_call_repr( 24 | fn: Callable[..., Any], args: Iterable[object], kwargs: Mapping[str, object] 25 | ) -> str: ... 26 | def getargspec( 27 | func: Union[types.FunctionType, types.MethodType, Callable[..., Any]], 28 | ) -> inspect.ArgSpec: ... 29 | def is_cython_or_generator(fn: object) -> bool: ... 30 | def is_cython_function(fn: object) -> bool: ... 31 | def is_cython_class(cls: Type[object]) -> bool: ... 32 | def is_classmethod(fn: object) -> bool: ... 33 | def wraps( 34 | wrapped: _AnyCallable, assigned: Sequence[str] = ..., updated: Sequence[str] = ... 35 | ) -> Callable[[_AnyCallable], _AnyCallable]: ... 36 | def get_subclass_tree(cls: Type[_T], ensure_unique: bool = ...) -> List[Type[_T]]: ... 37 | def lazy_stack() -> Iterable[types.FrameType]: ... 38 | -------------------------------------------------------------------------------- /qcore/microtime.pxd: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import cython 16 | from cpython cimport bool 17 | 18 | from . cimport inspection 19 | from .helpers cimport none, empty_tuple, empty_dict 20 | 21 | 22 | cdef object _time_offset 23 | 24 | cpdef inline object get_time_offset() 25 | cpdef inline object set_time_offset(object offset) 26 | cpdef inline object add_time_offset(object offset) 27 | 28 | cdef class TimeOffset: 29 | cdef object offset 30 | 31 | 32 | cpdef object utime() 33 | cpdef object true_utime() 34 | 35 | # NOTE: Can't cpdef this because of nested function. 36 | # 37 | # cpdef execute_with_timeout(fn, tuple args=?, dict kwargs=?, timeout=?, 38 | # bool fail_if_no_timer=?, 39 | # signal_type=?, 40 | # timer_type=?, 41 | # timeout_exception_cls=?) 42 | -------------------------------------------------------------------------------- /qcore/microtime.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | 17 | Helpers for dealing with sub-second time. 18 | 19 | Here, time is represented as an integer of microseconds, a.k.a. "Utime". 20 | 21 | """ 22 | 23 | __all__ = [ 24 | "DAY", 25 | "HOUR", 26 | "MICROSECOND", 27 | "MILLISECOND", 28 | "MINUTE", 29 | "MONTH_APPROXIMATE", 30 | "SECOND", 31 | "WEEK", 32 | "YEAR_APPROXIMATE", 33 | "TimeOffset", 34 | "Utime", 35 | "add_time_offset", 36 | "datetime_as_utime", 37 | "execute_with_timeout", 38 | "format_utime_as_iso_8601", 39 | "get_time_offset", 40 | "set_time_offset", 41 | "true_utime", 42 | "utime", 43 | "utime_as_datetime", 44 | "utime_delta", 45 | ] 46 | 47 | 48 | import signal 49 | from functools import wraps 50 | from time import time as _time_in_seconds 51 | from datetime import datetime, timezone 52 | from typing import NewType 53 | 54 | from . import inspection 55 | from .helpers import none, empty_tuple, empty_dict 56 | from .errors import TimeoutError, NotSupportedError 57 | 58 | 59 | Utime = NewType("Utime", int) 60 | 61 | 62 | MICROSECOND = 1 63 | MILLISECOND = MICROSECOND * 1000 64 | SECOND = MILLISECOND * 1000 65 | MINUTE = SECOND * 60 66 | HOUR = MINUTE * 60 67 | DAY = HOUR * 24 68 | WEEK = DAY * 7 69 | YEAR_APPROXIMATE = int(DAY * 365.25) 70 | MONTH_APPROXIMATE = int(YEAR_APPROXIMATE // 12) 71 | 72 | 73 | _offset_utime = 0 # type: Utime 74 | 75 | 76 | def utime_delta(*, days=0, hours=0, minutes=0, seconds=0): 77 | """Gets time delta in microseconds. 78 | 79 | Note: Do NOT use this function without keyword arguments. 80 | It will become much-much harder to add extra time ranges later if positional arguments are used. 81 | 82 | """ 83 | return (days * DAY) + (hours * HOUR) + (minutes * MINUTE) + (seconds * SECOND) 84 | 85 | 86 | def get_time_offset(): 87 | """Gets the offset applied to time() function result in microseconds.""" 88 | global _offset_utime 89 | return _offset_utime 90 | 91 | 92 | def set_time_offset(offset): 93 | """Sets the offset applied to time() function result in microseconds.""" 94 | global _offset_utime 95 | _offset_utime = int(offset) 96 | 97 | 98 | def add_time_offset(offset): 99 | """Adds specified number of microseconds to the offset applied to time() function result.""" 100 | global _offset_utime 101 | _offset_utime += int(offset) 102 | 103 | 104 | class TimeOffset: 105 | """Temporarily applies specified offset (in microseconds) to time() function result.""" 106 | 107 | def __init__(self, offset): 108 | self.offset = int(offset) 109 | 110 | def __enter__(self): 111 | global _offset_utime 112 | _offset_utime += self.offset 113 | 114 | def __exit__(self, typ, value, traceback): 115 | global _offset_utime 116 | _offset_utime -= self.offset 117 | 118 | 119 | def utime(): 120 | """Gets current time in microseconds from the epoch time w/applied offset.""" 121 | return _offset_utime + int(_time_in_seconds() * SECOND) 122 | 123 | 124 | def true_utime(): 125 | """Gets current time in microseconds from the epoch time.""" 126 | return int(_time_in_seconds() * SECOND) 127 | 128 | 129 | # =================================================== 130 | # Conversions to/from PY Date-Time 131 | # =================================================== 132 | 133 | 134 | def utime_as_datetime(utime, *, tz=timezone.utc): 135 | """Get Python datetime instance for the given microseconds time. 136 | 137 | This time refers to an absolute moment, given as microseconds from Unix Epoch. 138 | 139 | """ 140 | return datetime.fromtimestamp(utime / SECOND, tz=tz) 141 | 142 | 143 | def datetime_as_utime(dt): 144 | """Get the microseconds time for given Python datetime instance. 145 | 146 | This time refers to an absolute moment, given as microseconds from Unix Epoch. 147 | 148 | """ 149 | return int(dt.timestamp() * SECOND) 150 | 151 | 152 | # =================================================== 153 | # Conversions to/from ISO 8601 Date-Time 154 | # =================================================== 155 | 156 | 157 | def format_utime_as_iso_8601(utime, *, sep="T", drop_subseconds=False, tz=timezone.utc): 158 | """Get ISO 8601 Time string for the given microseconds time. 159 | 160 | Example output for the default UTC timezone: 161 | "2022-10-31T18:02:03.123456+00:00" 162 | 163 | """ 164 | timespec = "seconds" if drop_subseconds else "auto" 165 | return utime_as_datetime(utime, tz=tz).isoformat(sep=sep, timespec=timespec) 166 | 167 | 168 | # datetime.fromisoformat() is new in Python 3.7. 169 | if hasattr(datetime, "fromisoformat"): 170 | 171 | def iso_8601_as_utime(iso_datetime): 172 | """Get the microseconds time for given ISO 8601 Time string. 173 | 174 | Example input: 175 | "2022-11-01T01:02:03.123456+07:00" 176 | 177 | """ 178 | return datetime_as_utime(datetime.fromisoformat(iso_datetime)) 179 | 180 | __all__.append("iso_8601_as_utime") 181 | 182 | 183 | # =================================================== 184 | # Timeout API 185 | # =================================================== 186 | 187 | 188 | # Windows compatibility stuff 189 | _DEFAULT_SIGNAL_TYPE = signal.SIGALRM if hasattr(signal, "SIGALRM") else None 190 | _DEFAULT_TIMER_TYPE = signal.ITIMER_REAL if hasattr(signal, "ITIMER_REAL") else None 191 | 192 | 193 | def execute_with_timeout( 194 | fn, 195 | args=None, 196 | kwargs=None, 197 | timeout=None, 198 | fail_if_no_timer=True, 199 | signal_type=_DEFAULT_SIGNAL_TYPE, 200 | timer_type=_DEFAULT_TIMER_TYPE, 201 | timeout_exception_cls=TimeoutError, 202 | ): 203 | """ 204 | Executes specified function with timeout. Uses SIGALRM to interrupt it. 205 | 206 | :type fn: function 207 | :param fn: function to execute 208 | 209 | :type args: tuple 210 | :param args: function args 211 | 212 | :type kwargs: dict 213 | :param kwargs: function kwargs 214 | 215 | :type timeout: float 216 | :param timeout: timeout, seconds; 0 or None means no timeout 217 | 218 | :type fail_if_no_timer: bool 219 | :param fail_if_no_timer: fail, if timer is nor available; normally it's available only in the 220 | main thread 221 | 222 | :type signal_type: signalnum 223 | :param signal_type: type of signal to use (see signal module) 224 | 225 | :type timer_type: signal.ITIMER_REAL, signal.ITIMER_VIRTUAL or signal.ITIMER_PROF 226 | :param timer_type: type of timer to use (see signal module) 227 | 228 | :type timeout_exception_cls: class 229 | :param timeout_exception_cls: exception to throw in case of timeout 230 | 231 | :return: fn call result. 232 | 233 | """ 234 | if args is None: 235 | args = empty_tuple 236 | if kwargs is None: 237 | kwargs = empty_dict 238 | 239 | if timeout is None or timeout == 0 or signal_type is None or timer_type is None: 240 | return fn(*args, **kwargs) 241 | 242 | def signal_handler(signum, frame): 243 | raise timeout_exception_cls(inspection.get_function_call_str(fn, args, kwargs)) 244 | 245 | old_signal_handler = none 246 | timer_is_set = False 247 | try: 248 | try: 249 | old_signal_handler = signal.signal(signal_type, signal_handler) 250 | signal.setitimer(timer_type, timeout) 251 | timer_is_set = True 252 | except ValueError: 253 | if fail_if_no_timer: 254 | raise NotSupportedError( 255 | "Timer is not available; the code is probably invoked from outside" 256 | " the main thread." 257 | ) 258 | return fn(*args, **kwargs) 259 | finally: 260 | if timer_is_set: 261 | signal.setitimer(timer_type, 0) 262 | if old_signal_handler is not none: 263 | signal.signal(signal_type, old_signal_handler) 264 | -------------------------------------------------------------------------------- /qcore/microtime.pyi: -------------------------------------------------------------------------------- 1 | import sys 2 | from datetime import datetime, timezone, tzinfo 3 | from typing import ( 4 | Any, 5 | Callable, 6 | Iterable, 7 | Mapping, 8 | NewType, 9 | Optional, 10 | SupportsInt, 11 | Type, 12 | TypeVar, 13 | ) 14 | from types import TracebackType 15 | 16 | _T = TypeVar("_T") 17 | 18 | Utime = NewType("Utime", int) 19 | 20 | MICROSECOND: Utime 21 | MILLISECOND: Utime 22 | SECOND: Utime 23 | MINUTE: Utime 24 | HOUR: Utime 25 | DAY: Utime 26 | WEEK: Utime 27 | YEAR_APPROXIMATE: Utime 28 | MONTH_APPROXIMATE: Utime 29 | 30 | def utime_delta( 31 | *, days: int = ..., hours: int = ..., minutes: int = ..., seconds: int = ... 32 | ) -> Utime: ... 33 | def get_time_offset() -> Utime: ... 34 | def set_time_offset(offset: SupportsInt) -> None: ... 35 | def add_time_offset(offset: SupportsInt) -> None: ... 36 | 37 | class TimeOffset: 38 | """Temporarily applies specified offset (in microseconds) to time() function result.""" 39 | 40 | def __init__(self, offset: SupportsInt) -> None: ... 41 | def __enter__(self) -> None: ... 42 | def __exit__( 43 | self, 44 | typ: Optional[Type[BaseException]], 45 | value: Optional[BaseException], 46 | traceback: Optional[TracebackType], 47 | ) -> None: ... 48 | 49 | def utime() -> Utime: ... 50 | def true_utime() -> Utime: ... 51 | 52 | # =================================================== 53 | # Conversions to/from PY Date-Time 54 | # =================================================== 55 | 56 | def utime_as_datetime(utime: Utime, *, tz: tzinfo = timezone.utc) -> datetime: ... 57 | def datetime_as_utime(dt: datetime) -> Utime: ... 58 | 59 | # =================================================== 60 | # Conversions to/from ISO 8601 Date-Time 61 | # =================================================== 62 | 63 | def format_utime_as_iso_8601( 64 | utime: Utime, 65 | *, 66 | sep: str = "T", 67 | drop_subseconds: bool = False, 68 | tz: tzinfo = timezone.utc, 69 | ) -> str: ... 70 | 71 | # datetime.fromisoformat() is new in Python 3.7. 72 | if sys.version_info >= (3, 7): 73 | def iso_8601_as_utime(iso_datetime: str) -> Utime: ... 74 | 75 | # =================================================== 76 | # Timeout API 77 | # =================================================== 78 | 79 | def execute_with_timeout( 80 | fn: Callable[..., _T], 81 | args: Optional[Iterable[Any]] = ..., 82 | kwargs: Optional[Mapping[str, Any]] = ..., 83 | timeout: Optional[float] = ..., 84 | fail_if_no_timer: bool = ..., 85 | signal_type: int = ..., 86 | timer_type: int = ..., 87 | timeout_exception_cls: Type[BaseException] = ..., 88 | ) -> _T: ... 89 | -------------------------------------------------------------------------------- /qcore/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quora/qcore/f93ccc0f04858f1c606d3b95f05ff315f4fd47bf/qcore/py.typed -------------------------------------------------------------------------------- /qcore/testing.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | 17 | Module with assertion helpers. 18 | 19 | The advantages of using a method like 20 | 21 | assert_eq(expected, actual) 22 | 23 | instead of 24 | 25 | assert expected == actual 26 | 27 | include: 28 | 29 | 1 - On failures, assert_eq prints an informative message of the actual 30 | values compared (e.g. AssertionError: 1 != 2) for free, which makes it 31 | faster and easier to iterate on tests. 32 | 2 - In the context of refactors, basic asserts incorrectly shift the burden of 33 | adding printouts and writing good test code to people refactoring code 34 | rather than the person who initially wrote the code. 35 | 36 | """ 37 | 38 | __all__ = ["Anything", "GreaterEq"] 39 | 40 | 41 | # The unittest.py testing framework checks for this variable in a module to 42 | # filter out stack frames from that module from the test output, in order to 43 | # make the output more concise. 44 | # __unittest = 1 45 | 46 | import functools 47 | import inspect 48 | from unittest.case import SkipTest 49 | 50 | TEST_PREFIX = "test" 51 | 52 | 53 | class _Anything: 54 | def __eq__(self, other): 55 | return True 56 | 57 | def __ne__(self, other): 58 | return False 59 | 60 | def __repr__(self): 61 | return "" 62 | 63 | def __hash__(self): 64 | return 0 65 | 66 | 67 | Anything = _Anything() 68 | 69 | 70 | class GreaterEq: 71 | """Greater than or equal to some value. 72 | 73 | For example, assert_eq(GreaterEq(2), 3) and assert_eq(GreaterEq(2), 2) succeed, 74 | while assert_eq(GreaterEq(3), 2) fails. 75 | Useful if only equality asserts are supported or if we need to 76 | check inequality in a subfield as part of an assert_eq on an object that contains it. 77 | """ 78 | 79 | def __init__(self, val): 80 | self.val = val 81 | 82 | def __eq__(self, other): 83 | return other >= self.val 84 | 85 | def __ne__(self, other): 86 | return not self.__eq__(other) 87 | 88 | def __repr__(self): 89 | return "".format(self.val) 90 | 91 | 92 | def disabled(func_or_class): 93 | """Decorator to disable a test. 94 | 95 | Ensures that nose skips the test and that neither the test's setup nor 96 | teardown is executed. 97 | 98 | """ 99 | 100 | def decorate_func(func): 101 | @functools.wraps(func) 102 | def wrapper(*args, **kwargs): 103 | raise SkipTest 104 | 105 | return wrapper 106 | 107 | def decorate_class(class_): 108 | class_.setup = class_.teardown = lambda self: None 109 | class_.setup_method = class_.teardown_method = lambda self: None 110 | return decorate_all_test_methods(decorate_func)(class_) 111 | 112 | if inspect.isfunction(func_or_class): 113 | return decorate_func(func_or_class) 114 | elif inspect.isclass(func_or_class): 115 | return decorate_class(func_or_class) 116 | else: 117 | assert False, "Must be used as a function or class decorator" 118 | 119 | 120 | def decorate_all_test_methods(decorator): 121 | """Decorator to apply another decorator to all test methods of a class.""" 122 | 123 | # in python 3, unbound methods are just functions, so we also need to check for functions 124 | def predicate(member): 125 | return inspect.ismethod(member) or inspect.isfunction(member) 126 | 127 | def wrapper(cls): 128 | for name, m in inspect.getmembers(cls, predicate): 129 | if name.startswith(TEST_PREFIX): 130 | setattr(cls, name, decorator(m)) 131 | return cls 132 | 133 | return wrapper 134 | 135 | 136 | decorate_all_test_methods.__test__ = False 137 | 138 | 139 | def decorate_func_or_method_or_class(decorator): 140 | """Applies a decorator to a function, method, or all methods of a class. 141 | 142 | This is a decorator that is applied to a decorator to allow a 143 | function/method decorator to be applied to a class and have it act on all 144 | test methods of the class. 145 | 146 | """ 147 | 148 | def decorate(func_or_class): 149 | if inspect.isclass(func_or_class): 150 | return decorate_all_test_methods(decorator)(func_or_class) 151 | elif inspect.isfunction(func_or_class): 152 | return decorator(func_or_class) 153 | else: 154 | assert False, "Target of decorator must be function or class" 155 | 156 | return decorate 157 | 158 | 159 | decorate_func_or_method_or_class.__test__ = False 160 | -------------------------------------------------------------------------------- /qcore/testing.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Type 2 | 3 | TEST_PREFIX: str 4 | Anything: Any 5 | 6 | class GreaterEq: 7 | val: object 8 | def __init__(self, val: object) -> None: ... 9 | 10 | def disabled(func_or_class: Any) -> Any: ... 11 | def decorate_all_test_methods( 12 | decorator: Callable[[Callable[..., Any]], Any], 13 | ) -> Callable[[Type[object]], Type[object]]: ... 14 | def decorate_func_or_method_or_class( 15 | decorator: Callable[[Callable[..., Any]], Any], 16 | ) -> Callable[[Any], Any]: ... 17 | -------------------------------------------------------------------------------- /qcore/tests/test_asserts.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from collections import defaultdict 17 | 18 | from qcore.asserts import ( 19 | assert_eq, 20 | assert_ge, 21 | assert_gt, 22 | assert_is, 23 | assert_is_not, 24 | assert_le, 25 | assert_lt, 26 | assert_ne, 27 | assert_unordered_list_eq, 28 | AssertRaises, 29 | assert_not_in, 30 | assert_in, 31 | assert_dict_eq, 32 | assert_in_with_tolerance, 33 | # Strings 34 | assert_is_substring, 35 | assert_is_not_substring, 36 | assert_startswith, 37 | assert_endswith, 38 | ) 39 | 40 | 41 | def test_assert_eq(): 42 | assert_eq(1, 1) 43 | assert_eq("abc", "abc") 44 | assert_eq(None, None) 45 | assert_eq(1.0004, 1.0005, tolerance=0.001) 46 | assert_eq(1, 1.0, tolerance=0.001) 47 | 48 | 49 | def test_assert_eq_failures(): 50 | with AssertRaises(AssertionError): 51 | assert_eq(1, 2) 52 | with AssertRaises(AssertionError): 53 | assert_eq("abc", "abcd") 54 | with AssertRaises(AssertionError): 55 | assert_eq(None, 1) 56 | with AssertRaises(AssertionError): 57 | assert_eq(1.0004, 1.0005, tolerance=0.00001) 58 | with AssertRaises(AssertionError): 59 | assert_eq(1, 1.01, tolerance=0.001) 60 | 61 | # type errors 62 | with AssertRaises(AssertionError): 63 | assert_eq("s", 1, tolerance=0.1) 64 | with AssertRaises(AssertionError): 65 | assert_eq(None, 1, tolerance=0.1) 66 | with AssertRaises(AssertionError): 67 | assert_eq(1, 1, tolerance="s") 68 | 69 | 70 | def test_assert_ordering(): 71 | # ints 72 | assert_gt(2, 1) 73 | with AssertRaises(AssertionError): 74 | assert_gt(1, 1) 75 | with AssertRaises(AssertionError): 76 | assert_gt(0, 1) 77 | 78 | assert_ge(2, 1) 79 | assert_ge(1, 1) 80 | with AssertRaises(AssertionError): 81 | assert_ge(0, 1) 82 | 83 | with AssertRaises(AssertionError): 84 | assert_lt(2, 1) 85 | with AssertRaises(AssertionError): 86 | assert_lt(1, 1) 87 | assert_lt(0, 1) 88 | 89 | with AssertRaises(AssertionError): 90 | assert_le(2, 1) 91 | assert_le(1, 1) 92 | assert_lt(0, 1) 93 | 94 | # floats (tolerance isn't supported) 95 | assert_gt(2.5, 1.5) 96 | with AssertRaises(AssertionError): 97 | assert_gt(1.5, 1.5) 98 | with AssertRaises(AssertionError): 99 | assert_gt(0.5, 1.5) 100 | 101 | assert_ge(2.5, 1.5) 102 | assert_ge(1.5, 1.5) 103 | with AssertRaises(AssertionError): 104 | assert_ge(0.5, 1.5) 105 | 106 | with AssertRaises(AssertionError): 107 | assert_lt(2.5, 1.5) 108 | with AssertRaises(AssertionError): 109 | assert_lt(1.5, 1.5) 110 | assert_lt(0.5, 1.5) 111 | 112 | with AssertRaises(AssertionError): 113 | assert_le(2.5, 1.5) 114 | assert_le(1.5, 1.5) 115 | assert_lt(0.5, 1.5) 116 | 117 | # strings 118 | assert_gt("c", "b") 119 | with AssertRaises(AssertionError): 120 | assert_gt("b", "b") 121 | with AssertRaises(AssertionError): 122 | assert_gt("a", "b") 123 | 124 | assert_ge("c", "b") 125 | assert_ge("b", "b") 126 | with AssertRaises(AssertionError): 127 | assert_ge("a", "b") 128 | 129 | with AssertRaises(AssertionError): 130 | assert_lt("c", "b") 131 | with AssertRaises(AssertionError): 132 | assert_lt("b", "b") 133 | assert_lt("a", "b") 134 | 135 | with AssertRaises(AssertionError): 136 | assert_le("c", "b") 137 | assert_le("b", "b") 138 | assert_lt("a", "b") 139 | 140 | 141 | def test_assert_ne(): 142 | assert_ne(1, 2) 143 | assert_ne("abc", "abcd") 144 | assert_ne(None, 1) 145 | assert_ne(1.0004, 1.0005, tolerance=0.00001) 146 | assert_ne(1, 1.01, tolerance=0.001) 147 | 148 | 149 | def test_assert_ne_with_failures(): 150 | with AssertRaises(AssertionError): 151 | assert_ne(1, 1) 152 | with AssertRaises(AssertionError): 153 | assert_ne("abc", "abc") 154 | with AssertRaises(AssertionError): 155 | assert_ne(None, None) 156 | with AssertRaises(AssertionError): 157 | assert_ne(1.0004, 1.0005, tolerance=0.001) 158 | with AssertRaises(AssertionError): 159 | assert_ne(1, 1.0, tolerance=0.001) 160 | 161 | # type errors 162 | with AssertRaises(AssertionError): 163 | assert_ne("s", 1, tolerance=0.1) 164 | with AssertRaises(AssertionError): 165 | assert_ne(None, 1, tolerance=0.1) 166 | with AssertRaises(AssertionError): 167 | assert_ne(1, 1, tolerance="s") 168 | 169 | 170 | def test_assert_is(): 171 | # Assign to val to make the assertion look more prototypical. 172 | val = None 173 | assert_is(None, val) 174 | assert_is(int, type(1)) 175 | 176 | with AssertRaises(AssertionError): 177 | assert_is(None, 1) 178 | with AssertRaises(AssertionError): 179 | assert_is(int, type("s")) 180 | 181 | 182 | def test_assert_is_not(): 183 | assert_is_not(None, 1) 184 | assert_is_not(int, type("s")) 185 | 186 | # Assign to val to make the assertion look more prototypical. 187 | val = None 188 | with AssertRaises(AssertionError): 189 | assert_is_not(None, val) 190 | with AssertRaises(AssertionError): 191 | assert_is_not(int, type(1)) 192 | 193 | 194 | def test_assert_not_in(): 195 | # test truncation of very long strings 196 | seq = "a" * 1000 + "bbb" + "a" * 1000 197 | with AssertRaises(AssertionError) as ar: 198 | assert_not_in("bbb", seq) 199 | e = ar.expected_exception_found 200 | assert_eq( 201 | "'bbb' is in '(truncated) ...%sbbb%s... (truncated)'" % ("a" * 50, "a" * 50), 202 | str(e), 203 | ) 204 | 205 | # same as above when the match is at index 0 206 | seq = "a" * 1000 207 | with AssertRaises(AssertionError) as ar: 208 | assert_not_in("aaa", seq) 209 | e = ar.expected_exception_found 210 | assert_eq("'aaa' is in 'aaa%s... (truncated)'" % ("a" * 50), str(e)) 211 | 212 | 213 | def test_assert_use_ascii_representation(): 214 | non_ascii_string = "Hello سلام" 215 | with AssertRaises(AssertionError) as ar: 216 | assert_eq("aaa", non_ascii_string) 217 | e = ar.expected_exception_found 218 | assert_eq("'aaa' != 'Hello \\u0633\\u0644\\u0627\\u0645'", str(e)) 219 | 220 | 221 | class SpecificException(Exception): 222 | pass 223 | 224 | 225 | class SpecificException2(Exception): 226 | pass 227 | 228 | 229 | class TestAssertRaises: 230 | def test_handles_specific_exceptions(self): 231 | with AssertRaises(SpecificException, SpecificException2): 232 | raise SpecificException("foo") 233 | 234 | def test_handles_any_exceptions(self): 235 | with AssertRaises(Exception): 236 | raise Exception("foo") 237 | 238 | def test_fails_if_raise_wrong_exception(self): 239 | with AssertRaises(AssertionError): 240 | with AssertRaises(SpecificException): 241 | raise Exception("foo") 242 | 243 | def test_fails_if_exception_not_raised(self): 244 | try: 245 | with AssertRaises(ValueError): 246 | pass 247 | except AssertionError: 248 | pass 249 | else: 250 | assert False, "expected an exception to be raised" 251 | 252 | def test_handles_multiple_exception_types(self): 253 | with AssertRaises(IndexError, AssertionError): 254 | assert False 255 | 256 | with AssertRaises(AssertionError, IndexError): 257 | print([][0]) 258 | 259 | class Assert2(AssertionError): 260 | pass 261 | 262 | class Index2(IndexError): 263 | pass 264 | 265 | with AssertRaises(AssertionError, IndexError): 266 | raise Assert2("foo") 267 | 268 | with AssertRaises(AssertionError, IndexError): 269 | raise Index2("foo") 270 | 271 | def test_with_extra(self): 272 | with AssertRaises(AssertionError) as ar: 273 | with AssertRaises(AssertionError, extra="extra message"): 274 | pass 275 | e = ar.expected_exception_found 276 | assert_in("extra message", str(e)) 277 | 278 | def test_no_extra_kwargs(self): 279 | with AssertRaises(AssertionError): 280 | with AssertRaises(NotImplementedError, not_valid_kwarg=None): 281 | pass 282 | 283 | 284 | def test_complex_assertions(): 285 | with AssertRaises(AssertionError): 286 | with AssertRaises(AssertionError): 287 | pass 288 | 289 | with AssertRaises(RuntimeError): 290 | raise RuntimeError() 291 | 292 | assert_unordered_list_eq([1, 2, 2], [2, 1, 2]) 293 | with AssertRaises(AssertionError): 294 | try: 295 | assert_unordered_list_eq([1, 2, 2], [2, 1]) 296 | except AssertionError as e: 297 | print(repr(e)) 298 | raise 299 | 300 | 301 | def test_string_assertions(): 302 | assert_is_substring("a", "bca") 303 | with AssertRaises(AssertionError): 304 | assert_is_substring("a", "bc") 305 | 306 | assert_is_not_substring("a", "bc") 307 | with AssertRaises(AssertionError): 308 | assert_is_not_substring("a", "bca") 309 | 310 | assert_startswith("a", "abc bcd") 311 | with AssertRaises(AssertionError): 312 | assert_startswith("b", "abc bcd") 313 | 314 | assert_endswith("d", "abc bcd") 315 | with AssertRaises(AssertionError): 316 | assert_endswith("c", "abc bcd") 317 | 318 | 319 | class ExceptionWithValue(Exception): 320 | def __init__(self, value): 321 | self.value = value 322 | 323 | 324 | def test_assert_error_saves_exception(): 325 | assertion = AssertRaises(ExceptionWithValue) 326 | with assertion: 327 | raise ExceptionWithValue(5) 328 | assert_eq(5, assertion.expected_exception_found.value) 329 | 330 | 331 | def test_message(): 332 | try: 333 | assert_is(1, None, message="custom message") 334 | except AssertionError as e: 335 | assert_in("custom message", str(e)) 336 | else: 337 | assert False, "should have thrown assertion error" 338 | 339 | 340 | def test_extra(): 341 | try: 342 | assert_eq("thing1", "thing2", extra="something extra") 343 | except AssertionError as e: 344 | assert_in("thing1", str(e)) 345 | assert_in("thing2", str(e)) 346 | assert_in("something extra", str(e)) 347 | else: 348 | assert False, "should have thrown assertion error" 349 | 350 | 351 | def test_assert_dict_eq(): 352 | assert_dict_eq({"a": 1}, {"a": 1}) 353 | 354 | with AssertRaises(AssertionError): 355 | assert_dict_eq({"a": 1}, {"b": 1}) 356 | with AssertRaises(AssertionError): 357 | assert_dict_eq({"a": "abc"}, {"a": "xyz"}) 358 | 359 | try: 360 | assert_dict_eq({"a": {"b": {"c": 1}}}, {"a": {"b": {"d": 1}}}) 361 | except AssertionError as e: 362 | assert_in("'a'->'b'", str(e)) 363 | else: 364 | assert False, "should have thrown assertion error" 365 | 366 | dd = defaultdict(lambda: defaultdict(lambda: defaultdict(int))) 367 | dd["a"]["b"]["c"] = 1 368 | assert_dict_eq({"a": {"b": {"c": 1}}}, dd) 369 | assert_dict_eq(dd, {"a": {"b": {"c": 1}}}) 370 | 371 | 372 | def test_assert_in_with_tolerance(): 373 | assert_in_with_tolerance(1, [1, 2, 3], 0) 374 | with AssertRaises(AssertionError): 375 | assert_in_with_tolerance(1, [2, 2, 2], 0) 376 | assert_in_with_tolerance(1, [2, 2, 2], 1) 377 | -------------------------------------------------------------------------------- /qcore/tests/test_debug.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import qcore 16 | from qcore.asserts import AssertRaises, assert_is_substring, assert_is, assert_eq 17 | from qcore.debug import get_bool_by_mask, set_by_mask 18 | from unittest import mock 19 | 20 | 21 | def test_hang_me_does_not_throw(): 22 | qcore.debug.hang_me(0) 23 | with mock.patch("time.sleep") as mock_sleep: 24 | qcore.debug.hang_me(1) 25 | mock_sleep.assert_called_once_with(1) 26 | mock_sleep.reset_mock() 27 | qcore.debug.hang_me() 28 | mock_sleep.assert_called_once_with(10000) 29 | 30 | 31 | def test_hange_me_handles_exception(): 32 | with mock.patch("time.sleep") as mock_sleep: 33 | mock_sleep.side_effect = RuntimeError 34 | with AssertRaises(RuntimeError): 35 | qcore.debug.hang_me() 36 | mock_sleep.side_effect = KeyboardInterrupt 37 | qcore.debug.hang_me() 38 | 39 | 40 | def test_format_stack(): 41 | def foo(): 42 | return qcore.debug.format_stack() 43 | 44 | st = foo() 45 | assert_is_substring("in foo\n", st) 46 | 47 | 48 | def test_debug_counter(): 49 | counter = qcore.debug.counter("test_debug_counter") 50 | counter_again = qcore.debug.counter("test_debug_counter") 51 | 52 | assert_is(counter, counter_again) 53 | counter.increment(5) 54 | assert_eq("DebugCounter('test_debug_counter', value=5)", str(counter)) 55 | assert_eq("DebugCounter('test_debug_counter', value=5)", repr(counter)) 56 | 57 | counter.decrement(3) 58 | assert_eq("DebugCounter('test_debug_counter', value=2)", str(counter)) 59 | assert_eq("DebugCounter('test_debug_counter', value=2)", repr(counter)) 60 | 61 | 62 | def test_bool_by_mask(): 63 | class MaskObject: 64 | def __init__(self): 65 | self.TEST_MASK_1 = False 66 | self.TEST_MASK_2 = True 67 | 68 | m = MaskObject() 69 | assert_is(True, get_bool_by_mask(m, "ABC")) 70 | assert_is(False, get_bool_by_mask(m, "TEST_MASK")) 71 | assert_is(False, get_bool_by_mask(m, "TEST_MASK_1")) 72 | assert_is(True, get_bool_by_mask(m, "TEST_MASK_2")) 73 | 74 | set_by_mask(m, "TEST_", True) 75 | assert_is(True, get_bool_by_mask(m, "TEST_MASK")) 76 | assert_is(True, get_bool_by_mask(m, "TEST_MASK_1")) 77 | assert_is(True, get_bool_by_mask(m, "TEST_MASK_2")) 78 | 79 | set_by_mask(m, "TEST_MASK_2", False) 80 | assert_is(True, get_bool_by_mask(m, "ABC")) 81 | assert_is(False, get_bool_by_mask(m, "TEST_MASK")) 82 | assert_is(True, get_bool_by_mask(m, "TEST_MASK_1")) 83 | assert_is(False, get_bool_by_mask(m, "TEST_MASK_2")) 84 | -------------------------------------------------------------------------------- /qcore/tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from qcore import ( 16 | convert_result, 17 | decorate, 18 | decorator_of_context_manager, 19 | DecoratorBase, 20 | deprecated, 21 | retry, 22 | get_original_fn, 23 | DecoratorBinder, 24 | ) 25 | from qcore.asserts import assert_eq, assert_is, assert_in, assert_ne, AssertRaises 26 | import inspect 27 | import pickle 28 | from unittest import mock 29 | 30 | 31 | @deprecated("Deprecated.") 32 | def deprecated_fn(): 33 | pass 34 | 35 | 36 | @deprecated("Deprecated.") 37 | class DeprecatedClass: 38 | pass 39 | 40 | 41 | def test_deprecated(): 42 | fn = deprecated_fn 43 | fn.__doc__.startswith("Deprecated") 44 | 45 | # should not fail 46 | deprecated("Deprecated.")(3) 47 | 48 | 49 | def test_convert_result(): 50 | @convert_result(list) 51 | def test1(*args): 52 | for a in args: 53 | yield a 54 | 55 | result = test1(1, 2) 56 | assert_is(list, result.__class__) 57 | assert_eq([1, 2], result) 58 | 59 | 60 | class AnyException(Exception): 61 | pass 62 | 63 | 64 | class AnyOtherException(Exception): 65 | pass 66 | 67 | 68 | @retry(Exception) 69 | def retry_it(): 70 | pass 71 | 72 | 73 | class TestRetry: 74 | def create_generator_function(self, exception_type, max_tries): 75 | fn_body = mock.Mock() 76 | fn_body.return_value = range(3) 77 | 78 | @retry(exception_type, max_tries=max_tries) 79 | def any_function(*args, **kwargs): 80 | for it in fn_body(*args, **kwargs): 81 | yield it 82 | 83 | return any_function, fn_body 84 | 85 | def create_any_function(self, exception_type, max_tries): 86 | fn_body = mock.Mock() 87 | fn_body.return_value = [] 88 | 89 | @retry(exception_type, max_tries=max_tries) 90 | def any_function(*args, **kwargs): 91 | return fn_body(*args, **kwargs) 92 | 93 | return any_function, fn_body 94 | 95 | def test_pickling(self): 96 | for protocol in range(pickle.HIGHEST_PROTOCOL + 1): 97 | pickled = pickle.dumps(retry_it, protocol=protocol) 98 | assert_is(retry_it, pickle.loads(pickled)) 99 | 100 | def test_retry_passes_all_arguments(self): 101 | any_expected_exception_type = AnyException 102 | 103 | for method in (self.create_any_function, self.create_generator_function): 104 | any_function, fn_body = method(any_expected_exception_type, max_tries=2) 105 | list(any_function(1, 2, foo=3)) 106 | fn_body.assert_called_once_with(1, 2, foo=3) 107 | 108 | def test_retry_does_not_retry_on_no_exception(self): 109 | any_expected_exception_type = AnyException 110 | 111 | for method in (self.create_any_function, self.create_generator_function): 112 | any_function, fn_body = method(any_expected_exception_type, max_tries=3) 113 | list(any_function()) 114 | fn_body.assert_called_once_with() 115 | 116 | def test_retry_does_not_retry_on_unspecified_exception(self): 117 | any_expected_exception_type = AnyException 118 | any_unexpected_exception_type = AnyOtherException 119 | 120 | for method in (self.create_any_function, self.create_generator_function): 121 | any_function, fn_body = method(any_expected_exception_type, max_tries=3) 122 | fn_body.side_effect = any_unexpected_exception_type 123 | 124 | with AssertRaises(any_unexpected_exception_type): 125 | list(any_function()) 126 | 127 | fn_body.assert_called_once_with() 128 | 129 | def test_retry_retries_on_provided_exception(self): 130 | max_tries = 4 131 | any_expected_exception_type = AnyException 132 | 133 | for method in (self.create_any_function, self.create_generator_function): 134 | any_function, fn_body = method(any_expected_exception_type, max_tries) 135 | fn_body.side_effect = any_expected_exception_type 136 | 137 | with AssertRaises(any_expected_exception_type): 138 | list(any_function()) 139 | 140 | assert_eq(max_tries, fn_body.call_count) 141 | 142 | def test_retry_requires_max_try_at_least_one(self): 143 | any_expected_exception_type = AnyException 144 | for method in (self.create_any_function, self.create_generator_function): 145 | with AssertRaises(Exception): 146 | method(any_expected_exception_type, max_tries=0) 147 | method(any_expected_exception_type, max_tries=1) 148 | 149 | def test_retry_can_take_multiple_exceptions(self): 150 | max_tries = 4 151 | any_expected_exception_type = AnyException 152 | any_other_expected_exception_type = AnyOtherException 153 | 154 | expected_exceptions = ( 155 | any_expected_exception_type, 156 | any_other_expected_exception_type, 157 | ) 158 | 159 | for method in (self.create_any_function, self.create_generator_function): 160 | any_function, fn_body = method(expected_exceptions, max_tries) 161 | fn_body.side_effect = any_expected_exception_type 162 | 163 | with AssertRaises(any_expected_exception_type): 164 | list(any_function()) 165 | 166 | assert_eq(max_tries, fn_body.call_count) 167 | fn_body.reset_mock() 168 | 169 | fn_body.side_effect = any_other_expected_exception_type 170 | 171 | with AssertRaises(any_other_expected_exception_type): 172 | list(any_function()) 173 | 174 | assert_eq(max_tries, fn_body.call_count) 175 | 176 | def test_retry_preserves_argspec(self): 177 | def fn(foo, bar, baz=None, **kwargs): 178 | pass 179 | 180 | decorated = retry(Exception)(fn) 181 | 182 | assert_eq(inspect.signature(fn), inspect.signature(get_original_fn(decorated))) 183 | 184 | 185 | def test_decorator_of_context_manager(): 186 | data = [] 187 | 188 | class Context: 189 | "Dummy context" 190 | 191 | def __init__(self, key): 192 | self.key = key 193 | 194 | def __enter__(self): 195 | data.append("enter %s" % self.key) 196 | 197 | def __exit__(self, *args): 198 | data.append("exit %s" % self.key) 199 | 200 | decorator = decorator_of_context_manager(Context) 201 | 202 | @decorator("maras") 203 | def decorated(): 204 | data.append("inside maras") 205 | 206 | assert_eq("Dummy context", decorator.__doc__) 207 | 208 | decorated() 209 | 210 | assert_eq(["enter maras", "inside maras", "exit maras"], data) 211 | 212 | class NoDocString: 213 | def __enter__(self): 214 | pass 215 | 216 | def __exit__(self, *args): 217 | pass 218 | 219 | assert_eq( 220 | "Decorator that runs the inner function in the context of {}".format( 221 | NoDocString 222 | ), 223 | decorator_of_context_manager(NoDocString).__doc__, 224 | ) 225 | 226 | 227 | class UselessDecorator(DecoratorBase): 228 | def name(self): 229 | return "UselessDecorator" 230 | 231 | 232 | def useless_decorator(fn): 233 | return decorate(UselessDecorator)(fn) 234 | 235 | 236 | @useless_decorator 237 | def decorated_fn(): 238 | pass 239 | 240 | 241 | def test_decorated_fn_name(): 242 | # test that the decorator preserves the __module__ 243 | assert_in("test_decorators", decorated_fn.__module__) 244 | 245 | 246 | class CacheDecoratorBinder(DecoratorBinder): 247 | def dirty(self, *args): 248 | if self.instance is None: 249 | return self.decorator.dirty(*args) 250 | else: 251 | return self.decorator.dirty(self.instance, *args) 252 | 253 | 254 | class CacheDecorator(DecoratorBase): 255 | binder_cls = CacheDecoratorBinder 256 | 257 | def __init__(self, *args): 258 | super().__init__(*args) 259 | self._cache = {} 260 | 261 | def name(self): 262 | return "@cached" 263 | 264 | def dirty(self, *args): 265 | try: 266 | del self._cache[args] 267 | except KeyError: 268 | pass 269 | 270 | def __call__(self, *args): 271 | try: 272 | return self._cache[args] 273 | except KeyError: 274 | value = self.fn(*args) 275 | self._cache[args] = value 276 | return value 277 | 278 | 279 | cached = decorate(CacheDecorator) 280 | 281 | i = 0 282 | 283 | 284 | @cached 285 | def f(a, b): 286 | global i 287 | i += 1 288 | return a + b + i 289 | 290 | 291 | class CachedMethods: 292 | @cached 293 | def f(self, a, b): 294 | global i 295 | i += 1 296 | return a + b + i 297 | 298 | @cached 299 | @classmethod 300 | def cached_classmethod(cls, a, b): 301 | global i 302 | i += 1 303 | return a + b + i 304 | 305 | @cached 306 | @staticmethod 307 | def cached_staticmethod(a, b): 308 | global i 309 | i += 1 310 | return a + b + i 311 | 312 | @cached 313 | @cached 314 | @classmethod 315 | def double_cached(cls, a, b): 316 | global i 317 | i += 1 318 | return a + b + i 319 | 320 | def __str__(self): 321 | return "CachedMethods()" 322 | 323 | def __repr__(self): 324 | return str(self) 325 | 326 | 327 | class TestDecorators: 328 | def setup_method(self): 329 | global i 330 | i = 0 331 | 332 | def test_cached(self): 333 | global i 334 | instance = CachedMethods() 335 | methods = [ 336 | f, 337 | instance.f, 338 | CachedMethods.cached_classmethod, 339 | CachedMethods.cached_staticmethod, 340 | ] 341 | 342 | for method in methods: 343 | i = 0 344 | assert method.is_decorator() 345 | assert_eq("@cached", method.name()) 346 | assert_eq(3, method(1, 1)) 347 | assert_eq(3, method(1, 1)) 348 | method.dirty(2, 1) 349 | assert_eq(3, method(1, 1)) 350 | method.dirty(1, 1) 351 | assert_eq(4, method(1, 1)) 352 | 353 | def test_unbound_method(self): 354 | instance = CachedMethods() 355 | assert_eq(3, CachedMethods.f(instance, 1, 1)) 356 | assert_eq(3, CachedMethods.f(instance, 1, 1)) 357 | CachedMethods.f.dirty(instance, 1, 2) 358 | assert_eq(3, CachedMethods.f(instance, 1, 1)) 359 | CachedMethods.f.dirty(instance, 1, 1) 360 | assert_eq(4, CachedMethods.f(instance, 1, 1)) 361 | 362 | def test_decorator_str_and_repr(self): 363 | cases = [ 364 | (f, "@cached test_decorators.f"), 365 | (CachedMethods().f, "<@cached test_decorators.f bound to CachedMethods()>"), 366 | (CachedMethods.f, "<@cached test_decorators.f unbound>"), 367 | ( 368 | CachedMethods.cached_classmethod, 369 | ( 370 | "<@cached test_decorators.cached_classmethod bound to >" 372 | ), 373 | ), 374 | ( 375 | CachedMethods.cached_staticmethod, 376 | "@cached test_decorators.cached_staticmethod", 377 | ), 378 | ] 379 | for method, expected in cases: 380 | assert_eq(expected, str(method)) 381 | assert_eq(expected, repr(method)) 382 | 383 | def test_binder_equality(self): 384 | assert_eq(CachedMethods.f, CachedMethods.f) 385 | instance = CachedMethods() 386 | assert_eq(instance.f, instance.f) 387 | assert_ne(instance.f, CachedMethods.f) 388 | assert_eq(1, len({instance.f, instance.f})) 389 | 390 | def test_double_caching(self): 391 | assert_eq(3, CachedMethods.double_cached(1, 1)) 392 | assert_eq(3, CachedMethods.double_cached(1, 1)) 393 | CachedMethods.double_cached.dirty(1, 1) 394 | assert_eq(3, CachedMethods.double_cached(1, 1)) 395 | CachedMethods.double_cached.decorator.fn.dirty(CachedMethods, 1, 1) 396 | assert_eq(3, CachedMethods.double_cached(1, 1)) 397 | CachedMethods.double_cached.dirty(1, 1) 398 | CachedMethods.double_cached.decorator.fn.dirty(CachedMethods, 1, 1) 399 | assert_eq(4, CachedMethods.double_cached(1, 1)) 400 | 401 | def test_pickling(self): 402 | f.dirty(1, 1) 403 | pickled = pickle.dumps(f) 404 | unpickled = pickle.loads(pickled) 405 | assert_eq(3, unpickled(1, 1)) 406 | -------------------------------------------------------------------------------- /qcore/tests/test_disallow_inheritance.py: -------------------------------------------------------------------------------- 1 | import qcore 2 | from qcore.asserts import AssertRaises 3 | 4 | 5 | class Foo(metaclass=qcore.DisallowInheritance): 6 | pass 7 | 8 | 9 | def test_disallow_inheritance(): 10 | with AssertRaises(TypeError): 11 | 12 | class Bar(Foo): 13 | pass 14 | -------------------------------------------------------------------------------- /qcore/tests/test_enum.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | import pickle 17 | from qcore.enum import Enum, Flags, IntEnum, EnumValueGenerator 18 | from qcore.asserts import ( 19 | assert_eq, 20 | assert_is, 21 | assert_ne, 22 | assert_raises, 23 | assert_in, 24 | assert_not_in, 25 | assert_is_instance, 26 | ) 27 | 28 | 29 | class Gender(Enum): 30 | undefined = 0 31 | male = 1 32 | female = 2 33 | 34 | @property 35 | def opposite(self): 36 | assert self.is_valid() 37 | if self.value == 0: 38 | return Gender.undefined 39 | return Gender(3 - self.value) 40 | 41 | 42 | class SeparateEnum(IntEnum): 43 | undefined = 0 44 | male = 1 45 | female = 2 46 | 47 | 48 | def _assert_equality_both_directions(left, right, not_equal): 49 | assert_eq(left, right) 50 | assert_eq(right, left) 51 | assert_ne(not_equal, right) 52 | assert_ne(right, not_equal) 53 | 54 | 55 | def test_gender(): 56 | assert_eq([2, 1], Gender._flag_values) 57 | assert_eq([Gender.undefined, Gender.male, Gender.female], Gender.get_members()) 58 | assert_eq(["undefined", "male", "female"], Gender.get_names()) 59 | assert_eq(3, len(Gender)) 60 | 61 | _assert_equality_both_directions(0, Gender.undefined, 1) 62 | _assert_equality_both_directions(1, Gender.male, 2) 63 | _assert_equality_both_directions(2, Gender.female, 3) 64 | 65 | _assert_equality_both_directions( 66 | Gender.undefined, Gender.parse("undefined"), Gender.male 67 | ) 68 | _assert_equality_both_directions(Gender.male, Gender.parse("male"), Gender.female) 69 | _assert_equality_both_directions(Gender.female, Gender.parse("female"), Gender.male) 70 | 71 | _assert_equality_both_directions( 72 | Gender.undefined, Gender.parse(Gender.undefined), Gender.male 73 | ) 74 | _assert_equality_both_directions( 75 | Gender.male, Gender.parse(Gender.male), Gender.female 76 | ) 77 | _assert_equality_both_directions( 78 | Gender.female, Gender.parse(Gender.female), Gender.male 79 | ) 80 | 81 | assert_is(None, Gender.parse("na", None)) 82 | assert_raises(lambda: Gender.parse("na"), KeyError) 83 | assert_raises(lambda: Gender.parse(SeparateEnum.undefined), KeyError) 84 | assert_raises(lambda: Gender.parse(b"ni\xc3\xb1o"), KeyError) 85 | assert_raises(lambda: Gender.parse("ni\xf1o"), KeyError) 86 | assert_raises(lambda: Gender.parse(b"ni\xff\xffo"), KeyError) 87 | assert_raises(lambda: Gender.parse("\xe4\xb8\xad\xe6\x96\x87"), KeyError) 88 | 89 | assert_eq("undefined", Gender(0).short_name) 90 | assert_eq("male", Gender(1).short_name) 91 | assert_eq("female", Gender(2).short_name) 92 | 93 | assert_eq("Gender.female", Gender.female.long_name) 94 | assert_eq("Female", Gender.female.title) 95 | assert_eq("test_enum.Gender.female", Gender.female.full_name) 96 | 97 | assert_is(None, Gender.parse("", None)) 98 | assert_is(None, Gender.parse(4, None)) 99 | assert_raises(lambda: Gender.parse(""), KeyError) 100 | assert_raises(lambda: Gender.parse(4), KeyError) 101 | assert_is(None, Gender("", None)) 102 | assert_is(None, Gender(4, None)) 103 | assert_raises(lambda: Gender(""), KeyError) 104 | assert_raises(lambda: Gender(4), KeyError) 105 | 106 | assert_eq(str(Gender.male), "male") 107 | assert_eq(repr(Gender.male), "Gender.male") 108 | 109 | 110 | def test_property(): 111 | assert_eq(Gender.undefined.opposite, Gender.undefined) 112 | assert_eq(Gender.male.opposite, Gender.female) 113 | assert_eq(Gender.female.opposite, Gender.male) 114 | 115 | 116 | def test_create(): 117 | def test_exact_gender(cls): 118 | assert_eq([cls.male, cls.female], cls.get_members()) 119 | assert_eq([cls.male, cls.female], list(cls)) 120 | assert_eq(1, cls.male) 121 | assert_eq(2, cls.female) 122 | assert_eq(cls.male, Gender.male) 123 | assert_eq(cls.female, Gender.female) 124 | assert_in(cls.male, cls) 125 | 126 | assert_eq(cls.male, cls.parse(1)) 127 | assert_eq(cls.male, cls.parse("male")) 128 | assert_eq(cls.male, cls.parse(cls.male)) 129 | assert_eq(cls.male, cls(1)) 130 | assert_eq(cls.male, cls("male")) 131 | assert_eq(cls.male, cls(cls.male)) 132 | 133 | class ExactGender(Enum): 134 | male = Gender.male 135 | female = Gender.female 136 | 137 | test_exact_gender(ExactGender) 138 | 139 | cls = Enum.create("ExactGender", [Gender.male, Gender.female]) 140 | test_exact_gender(cls) 141 | 142 | cls = Enum.create("ExactGender", {"male": 1, "female": 2}) 143 | test_exact_gender(cls) 144 | 145 | cls = Enum.create("ExactGender", [("male", 1), ("female", 2)]) 146 | test_exact_gender(cls) 147 | 148 | 149 | class Xy(Flags): 150 | x = 1 151 | y = 4 152 | xy = 5 153 | 154 | 155 | class Xyz(Xy): 156 | z = 8 157 | 158 | 159 | class Xyzna(Xyz): 160 | na = 0 161 | 162 | 163 | def test_xyz(): 164 | assert_eq([8, 5, 4, 1], Xyz._flag_values) 165 | assert_eq([Xy.x, Xy.y, Xy.xy], Xy.get_members()) 166 | assert_eq([Xyz.x, Xyz.y, Xyz.xy, Xyz.z], Xyz.get_members()) 167 | assert_eq([Xyzna.na, Xyzna.x, Xyzna.y, Xyzna.xy, Xyzna.z], Xyzna.get_members()) 168 | assert_eq( 169 | "[Xyzna.na, Xyzna.x, Xyzna.y, Xyzna.xy, Xyzna.z]", str(Xyzna.get_members()) 170 | ) 171 | 172 | assert_eq(0, Xyz.parse("")) 173 | assert_eq(0, Xyz.parse(0)) 174 | assert_eq(0, Xyzna.parse("na")) 175 | assert_is(None, Xyz.parse("na", None)) 176 | assert_eq(0, Xyz("")) 177 | assert_eq(0, Xyz(0)) 178 | assert_eq(0, Xyzna("na")) 179 | assert_is(None, Xyz("na", None)) 180 | 181 | assert_raises(lambda: Xyz.parse("_"), KeyError) 182 | assert_raises(lambda: Xyz.parse("x,_"), KeyError) 183 | assert_raises(lambda: Xyz("_"), KeyError) 184 | assert_raises(lambda: Xyz("x,_"), KeyError) 185 | 186 | assert_eq(4, Xyz.parse("y")) 187 | assert_eq(4, Xyz.parse(4)) 188 | assert_eq(4, Xyz("y")) 189 | assert_eq(4, Xyz(4)) 190 | 191 | assert_eq(5, Xyz.parse("xy")) 192 | assert_eq(5, Xyz.parse("x,y")) 193 | assert_eq(5, Xyz("xy")) 194 | assert_eq(5, Xyz("x,y")) 195 | 196 | assert_is(None, Xyz.parse(100, None)) 197 | assert_raises(lambda: Xyz.parse(100), KeyError) 198 | assert_is(None, Xyz(100, None)) 199 | assert_raises(lambda: Xyz(100), KeyError) 200 | 201 | assert_eq("x", Xyz(1).short_name) 202 | assert_eq("y", Xyz(4).short_name) 203 | assert_eq("xy", Xyz(5).short_name) 204 | assert_eq("z,xy", Xyz(8 | 5).short_name) 205 | assert_eq("", Xyz(0).short_name) 206 | assert_eq("na", Xyzna(0).short_name) 207 | 208 | assert_eq("z", str(Xyz.z)) 209 | assert_eq("Xyz.z", repr(Xyz.z)) 210 | assert_eq("xy", str(Xyz.xy)) 211 | assert_eq("Xyz.xy", repr(Xyz.xy)) 212 | assert_eq("z,x", str(Xyz.x | Xyz.z)) 213 | assert_eq("Xyz.parse('z,x')", repr(Xyz.x | Xyz.z)) 214 | 215 | 216 | def test_instances(): 217 | assert_eq(0, Gender.undefined) 218 | assert_eq(1, Gender.male) 219 | assert_eq(2, Gender.female) 220 | 221 | assert_eq(0, Gender.undefined()) 222 | assert_eq(1, Gender.male()) 223 | assert_eq(2, Gender.female()) 224 | 225 | assert_eq(0, Gender.undefined.value) 226 | assert_eq(1, Gender.male.value) 227 | assert_eq(2, Gender.female.value) 228 | 229 | assert_eq(Gender(0), Gender.undefined) 230 | assert_eq(Gender(1), Gender.male) 231 | assert_eq(Gender(2), Gender.female) 232 | 233 | assert Gender(0).is_valid() 234 | 235 | g0 = Gender.parse(0) 236 | assert isinstance(g0, Gender) 237 | assert_eq(0, g0.value) 238 | g1 = Gender.parse(1) 239 | assert isinstance(g1, Gender) 240 | assert_eq(1, g1.value) 241 | assert_is(None, Gender.parse(4, None)) 242 | assert_raises(lambda: Gender.parse(4), KeyError) 243 | assert_eq(hash(2), hash(Gender(2))) 244 | 245 | assert_eq("xy", str(Xyz.xy)) 246 | assert_eq("Xyz.xy", repr(Xyz.xy)) 247 | 248 | assert_eq(Xyz.xy, Xyz.x | Xyz.y) 249 | assert_eq(5, Xyz.x | Xyz.y) 250 | assert_eq(5, Xyz.x | 4) 251 | assert_eq(5, Xyz.x | Xyz.y) 252 | 253 | assert_eq(Xyz.xy, Xyz.x | Xyz.y) 254 | assert_eq(8 | 5, Xyz.z | Xyz.xy) 255 | 256 | assert_eq(Xyzna.na, Xyz.x & Xyz.y) 257 | 258 | assert_in(Xyz.x, Xyz.xy) 259 | assert_in(Xyz.y, Xyz.xy) 260 | assert_in(Xyz.xy, Xyz.xy) 261 | assert_not_in(Xyz.z, Xyz.xy) 262 | assert_not_in(Xyz.z, Xyz.x) 263 | assert_not_in(Xyz.z, Xyz.y) 264 | 265 | assert_in(Xyz.x(), Xyz.xy) 266 | assert_in(Xyz.y(), Xyz.xy) 267 | assert_in(Xyz.xy(), Xyz.xy) 268 | assert_not_in(Xyz.z(), Xyz.xy) 269 | assert_not_in(Xyz.z(), Xyz.x) 270 | assert_not_in(Xyz.z(), Xyz.y) 271 | 272 | assert_in(Xyzna.na, Xyzna.x) 273 | assert_in(Xyzna.na, Xyzna.y) 274 | assert_in(Xyzna.na, Xyzna.xy) 275 | assert_in(Xyzna.na, Xyzna.z) 276 | assert_in(Xyzna.na, Xyzna.na) 277 | 278 | xyz1 = Xyz.parse("z,xy") 279 | xyz2 = Xyz.parse("x,y,z") 280 | xyz3 = Xyz.parse("xy,z") 281 | xyz4 = Xyz.parse(8 | 5) 282 | assert isinstance(xyz1, Xyz) 283 | assert isinstance(xyz2, Xyz) 284 | assert isinstance(xyz3, Xyz) 285 | assert isinstance(xyz4, Xyz) 286 | assert_eq(8 | 5, xyz1.value) 287 | assert_eq(8 | 5, xyz2.value) 288 | assert_eq(8 | 5, xyz3.value) 289 | assert_eq(8 | 5, xyz4.value) 290 | assert_is(None, Xyz.parse(100, None)) 291 | assert_raises(lambda: Xyz.parse(100), KeyError) 292 | 293 | na1 = Xyz.parse("") 294 | na2 = Xyzna.parse("na") 295 | na3 = Xyzna.parse("na") 296 | assert isinstance(na1, Xyz) 297 | assert isinstance(na2, Xyzna) 298 | assert isinstance(na3, Xyzna) 299 | assert_eq(0, na1) 300 | assert_eq(0, na2) 301 | assert_eq(0, na3) 302 | 303 | 304 | class Python(IntEnum): 305 | two = 2 306 | three = 3 307 | 308 | 309 | def test_intenum(): 310 | assert_is_instance(Python.two, int) 311 | assert_eq("Python.two", repr(Python.two)) 312 | assert_eq("2", json.dumps(Python.two)) 313 | assert_in(Python.two, Python) 314 | assert_in(2, Python) 315 | assert_not_in(4, Python) 316 | 317 | 318 | def test_generator(): 319 | enum_generator = EnumValueGenerator() 320 | assert_eq(1, enum_generator()) 321 | assert_eq(2, enum_generator()) 322 | 323 | enum_generator.reset(5) 324 | assert_eq(5, enum_generator()) 325 | assert_eq(6, enum_generator()) 326 | 327 | 328 | def test_bad_enum(): 329 | def declare_bad_enum(): 330 | class BadEnum(Enum): 331 | member1 = 1 332 | member2 = 1 333 | 334 | assert_raises(declare_bad_enum, AssertionError) 335 | 336 | 337 | class LongEnum(Enum): 338 | x = 100 339 | 340 | 341 | def test_long_enum(): 342 | assert_is_instance(LongEnum.x, LongEnum) 343 | 344 | 345 | # mypy doesn't recognize that Gender.male is a Gender instance 346 | DynamicEnum = Enum.create("DynamicEnum", [Gender.male, Gender.female]) # type: ignore 347 | 348 | 349 | def test_pickling(): 350 | assert_eq(Gender.female, pickle.loads(pickle.dumps(Gender.female))) 351 | assert_eq(DynamicEnum.male, pickle.loads(pickle.dumps(DynamicEnum.male))) 352 | # results of pickling Gender.male in Python 3 with protocol 0 and 2 353 | proto0 = b"c__builtin__\ngetattr\np0\n(ctest_enum\nGender\np1\nVparse\np2\ntp3\nRp4\n(L1L\ntp5\nRp6\n." 354 | proto2 = b"\x80\x02c__builtin__\ngetattr\nq\x00ctest_enum\nGender\nq\x01X\x05\x00\x00\x00parseq\x02\x86q\x03Rq\x04K\x01\x85q\x05Rq\x06." 355 | assert_eq(Gender.male, pickle.loads(proto0)) 356 | assert_eq(Gender.male, pickle.loads(proto2)) 357 | -------------------------------------------------------------------------------- /qcore/tests/test_errors.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import sys 16 | import traceback 17 | 18 | from qcore import errors 19 | from qcore.asserts import assert_in 20 | 21 | 22 | def test_errors(): 23 | def f1(): 24 | assert 0, "holy moly" 25 | 26 | def raise_later(e): 27 | errors.reraise(e) 28 | 29 | def f2(): 30 | try: 31 | f1() 32 | except AssertionError as e: 33 | prepared_e = errors.prepare_for_reraise(e) 34 | else: 35 | assert False, "f1 should have raised AssertionError" 36 | raise_later(prepared_e) 37 | 38 | try: 39 | f2() 40 | except AssertionError: 41 | formatted = traceback.format_tb(sys.exc_info()[2]) 42 | formatted_message = "".join(formatted) 43 | assert_in("holy moly", formatted_message) 44 | assert_in("f1", formatted_message) 45 | else: 46 | assert False, "f2 should have raised AssertionError" 47 | -------------------------------------------------------------------------------- /qcore/tests/test_events.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import qcore 16 | from qcore.asserts import ( 17 | AssertRaises, 18 | assert_eq, 19 | assert_in, 20 | assert_is, 21 | assert_not_in, 22 | assert_is_instance, 23 | ) 24 | 25 | from unittest import mock 26 | 27 | 28 | count = 0 29 | 30 | 31 | def handler(expected_count, raise_error=False): 32 | global count 33 | assert_eq(count, expected_count) 34 | count += 1 35 | if raise_error: 36 | raise NotImplementedError() 37 | 38 | 39 | def test_events(): 40 | global count 41 | h0 = lambda: handler(0) 42 | h1 = lambda: handler(1) 43 | h2 = lambda: handler(2) 44 | h0e = lambda: handler(0, True) 45 | 46 | count = 0 47 | events = qcore.EventHook() 48 | assert_eq("EventHook()", str(events)) 49 | assert_eq("EventHook()", repr(events)) 50 | 51 | events.subscribe(h0) 52 | assert_eq("EventHook(%r,)" % h0, str(events)) 53 | assert_eq("EventHook(%r,)" % h0, repr(events)) 54 | 55 | events() 56 | assert_eq(count, 1) 57 | 58 | count = 0 59 | events = qcore.EventHook() 60 | assert_eq([], list(events)) 61 | events.subscribe(h0) 62 | events.subscribe(h1) 63 | assert_eq([h0, h1], list(events)) 64 | assert_in(h0, events) 65 | assert_in(h1, events) 66 | assert_not_in(h2, events) 67 | 68 | events() 69 | assert_eq(count, 2) 70 | 71 | count = 0 72 | events = qcore.EventHook() 73 | events.subscribe(h0e) 74 | events.subscribe(h1) 75 | try: 76 | events() 77 | except BaseException: 78 | pass 79 | assert_eq(count, 1) 80 | 81 | count = 0 82 | events = qcore.EventHook() 83 | events.subscribe(h0e) 84 | events.subscribe(h1) 85 | try: 86 | events.safe_trigger() 87 | except BaseException: 88 | pass 89 | assert_eq(count, 2) 90 | 91 | count = 0 92 | events = qcore.EventHook() 93 | events.subscribe(h0) 94 | events.subscribe(h1) 95 | events() 96 | assert_eq(count, 2) 97 | count = 0 98 | events.unsubscribe(h1) 99 | events() 100 | assert_eq(count, 1) 101 | count = 0 102 | events.unsubscribe(h0) 103 | events() 104 | assert_eq(count, 0) 105 | 106 | events = qcore.EventHook() 107 | events.subscribe(h0) 108 | events.subscribe(h1) 109 | events.unsubscribe(h0) 110 | count = 1 111 | events() 112 | assert_eq(count, 2) 113 | count = 0 114 | events.unsubscribe(h1) 115 | events() 116 | assert_eq(count, 0) 117 | 118 | events = qcore.EventHook() 119 | events.subscribe(h0) 120 | events.subscribe(h1) 121 | events.subscribe(h2) 122 | events.unsubscribe(h1) 123 | events.unsubscribe(h0) 124 | events.unsubscribe(h2) 125 | count = 0 126 | events() 127 | assert_eq(count, 0) 128 | 129 | 130 | def test_sinking_event_hook(): 131 | def failing_handler(): 132 | raise RuntimeError 133 | 134 | events = qcore.events.SinkingEventHook() 135 | assert_eq([], list(events)) 136 | events.subscribe(failing_handler) 137 | assert_eq([], list(events)) 138 | events.unsubscribe(failing_handler) 139 | assert_eq([], list(events)) 140 | 141 | events.trigger() 142 | events.safe_trigger() 143 | events() 144 | 145 | assert_not_in(None, events) 146 | assert_not_in(False, events) 147 | assert_eq("SinkingEventHook()", str(events)) 148 | 149 | 150 | def test_event_interceptor(): 151 | global count 152 | count = 0 153 | hub = qcore.EventHub() 154 | 155 | hub.on_a.trigger() 156 | hub.on_b.trigger() 157 | assert_eq(0, count) 158 | assert_eq([], list(hub.on_a)) 159 | assert_eq([], list(hub.on_b)) 160 | 161 | a_handler = lambda: handler(0) 162 | b_handler = lambda: handler(1) 163 | 164 | with qcore.EventInterceptor(hub, on_a=a_handler, on_b=b_handler): 165 | assert_eq([a_handler], list(hub.on_a)) 166 | assert_eq([b_handler], list(hub.on_b)) 167 | 168 | hub.on_a.trigger() 169 | hub.on_b.trigger() 170 | assert_eq(2, count) 171 | 172 | assert_eq([], list(hub.on_a)) 173 | assert_eq([], list(hub.on_b)) 174 | hub.on_a.trigger() 175 | hub.on_b.trigger() 176 | assert_eq(2, count) 177 | 178 | 179 | def test_event_hub(): 180 | h = qcore.EventHub() 181 | 182 | assert_eq(0, len(h)) 183 | assert_eq("EventHub({})", repr(h)) 184 | 185 | with AssertRaises(AttributeError): 186 | h.doesnt_start_with_on 187 | 188 | h_e = h.on_e 189 | assert_is_instance(h_e, qcore.EventHook) 190 | assert_eq(1, len(h)) 191 | assert_is(h_e, h["e"]) 192 | assert_eq("EventHub({'e': %r})" % h_e, repr(h)) 193 | assert_is(h, h.safe_trigger("e")) 194 | assert_is(h, h.trigger("e")) 195 | 196 | h_e.subscribe(lambda: 0) 197 | 198 | assert_in("e", h) 199 | assert_not_in("f", h) 200 | 201 | h["f"] = None 202 | assert_is(None, h["f"]) 203 | assert_in("f", h) 204 | assert_eq(2, len(h)) 205 | 206 | del h["f"] 207 | assert_not_in("f", h) 208 | assert_eq(1, len(h)) 209 | 210 | for k, v in h: 211 | assert_eq("e", k) 212 | assert_is(h_e, v) 213 | 214 | def bad_fn(*args): 215 | raise NotImplementedError() 216 | 217 | m = mock.MagicMock() 218 | h.on_test.subscribe(bad_fn) 219 | with AssertRaises(NotImplementedError): 220 | h.on("test", m).safe_trigger("test", 1) 221 | m.assert_called_once_with(1) 222 | m.reset_mock() 223 | 224 | h.off("test", bad_fn).trigger("test", 2, 3) 225 | m.assert_called_once_with(2, 3) 226 | 227 | 228 | def test_events_hub_with_source(): 229 | def handler(): 230 | pass 231 | 232 | hook = qcore.EventHook() 233 | hook.subscribe(handler) 234 | assert_eq([handler], list(hook)) 235 | 236 | hub = qcore.EventHub(source={"on_something": hook}) 237 | assert_eq([handler], list(hub.on_something)) 238 | 239 | 240 | def test_global_events(): 241 | c = len(qcore.events.hub) 242 | 243 | event = "test_global_event_4849tcj5" 244 | e = qcore.events.hub.get_or_create(event) 245 | 246 | event_fire_args = [] 247 | 248 | def event_handler(*args): 249 | event_fire_args.append(args) 250 | 251 | e.subscribe(event_handler) 252 | e() 253 | 254 | assert_eq(1, len(event_fire_args)) 255 | 256 | del qcore.events.hub[event] 257 | assert_eq(c, len(qcore.events.hub)) 258 | 259 | 260 | def test_enum_based_event_hub(): 261 | class Events(qcore.Enum): 262 | work = 1 263 | sleep = 2 264 | 265 | class MoreEvents(qcore.Enum): 266 | eat = 3 267 | 268 | class EventHub1(qcore.EnumBasedEventHub): 269 | __based_on__ = Events 270 | on_work = qcore.EventHook() 271 | on_sleep = qcore.EventHook() 272 | 273 | m1 = mock.MagicMock() 274 | m2 = mock.MagicMock() 275 | hub1 = EventHub1() 276 | hub1.on_work.subscribe(m1) 277 | hub1.on(Events.work, m2).trigger(Events.work) 278 | m1.assert_called_once_with() 279 | m2.assert_called_once_with() 280 | 281 | m1.reset_mock() 282 | m2.reset_mock() 283 | hub1.on(Events.sleep, lambda: None).trigger(Events.sleep) 284 | assert_eq(0, m1.call_count) 285 | assert_eq(0, m2.call_count) 286 | 287 | class EventHub2(qcore.EnumBasedEventHub): 288 | __based_on__ = [Events, MoreEvents] 289 | on_work = qcore.EventHook() 290 | on_sleep = qcore.EventHook() 291 | on_eat = qcore.EventHook() 292 | on_some_other_member = None 293 | 294 | def some_method(self): 295 | pass 296 | 297 | with AssertRaises(AssertionError): 298 | 299 | class BadEventHub3(qcore.EnumBasedEventHub): 300 | # No __based_on__ = [Events] 301 | pass 302 | 303 | with AssertRaises(AssertionError): 304 | 305 | class BadEventHub4(qcore.EnumBasedEventHub): 306 | __based_on__ = [Events, MoreEvents] 307 | on_work = qcore.EventHook() 308 | on_eat = qcore.EventHook() 309 | 310 | with AssertRaises(AssertionError): 311 | 312 | class BadEventHub5(qcore.EnumBasedEventHub): 313 | __based_on__ = [Events, MoreEvents] 314 | on_work = qcore.EventHook() 315 | on_sleep = qcore.EventHook() 316 | on_eat = qcore.EventHook() 317 | on_bad = qcore.EventHook() 318 | 319 | with AssertRaises(AssertionError): 320 | 321 | class BadEventHub6(qcore.EnumBasedEventHub): 322 | __based_on__ = Events 323 | on_work = qcore.EventHook() 324 | on_sleep = 1 325 | 326 | with AssertRaises(AssertionError): 327 | 328 | class BadEventHub7(qcore.EnumBasedEventHub): 329 | __based_on__ = [Events, MoreEvents, MoreEvents] # Duplicate members 330 | on_work = qcore.EventHook() 331 | on_sleep = qcore.EventHook() 332 | on_eat = qcore.EventHook() 333 | -------------------------------------------------------------------------------- /qcore/tests/test_examples.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import qcore 16 | from qcore.asserts import assert_eq, assert_is_not, AssertRaises 17 | 18 | 19 | def dump(*args): 20 | print(repr(args)) 21 | 22 | 23 | def fail(*args): 24 | raise NotImplementedError() 25 | 26 | 27 | def test_common(): 28 | o = qcore.MarkerObject("o @ qcore.test_examples") 29 | assert_is_not(o, qcore.miss) 30 | 31 | o = qcore.dict_to_object({"a": 1, "b": 2}) 32 | assert_eq(o.a, 1) # Zero overhead! 33 | assert_eq(o.b, 2) # Zero overhead! 34 | 35 | 36 | def test_events(): 37 | e = qcore.EventHook() 38 | e.subscribe(lambda *args: dump("Handler 1", *args)) 39 | e("argument") 40 | 41 | e.subscribe(lambda: fail("Handler 2")) 42 | e.subscribe(lambda: dump("Handler 3")) 43 | 44 | with AssertRaises(NotImplementedError): 45 | e() # prints 'Handler 1' 46 | 47 | with AssertRaises(NotImplementedError): 48 | e.safe_trigger() # prints 'Handler 1', 'Handler 3' 49 | 50 | h = qcore.EventHub() 51 | h.on_some_event.subscribe(lambda: dump("On some event")) 52 | h.on_some_other_event.subscribe(lambda: dump("On some other event")) 53 | 54 | h.on_some_event() 55 | h.on_some_other_event() 56 | 57 | # a.events.hub.on_this_unused_handler.subscribe(lambda: None) 58 | # qcore.events.hub.on_this_unused_handler.subscribe(lambda: None) 59 | -------------------------------------------------------------------------------- /qcore/tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import qcore 16 | from qcore.asserts import ( 17 | assert_eq, 18 | assert_is, 19 | assert_ne, 20 | assert_is_not, 21 | assert_is_substring, 22 | AssertRaises, 23 | ) 24 | 25 | # this import is just for test_object_from_string 26 | from qcore import asserts as asserts_ 27 | 28 | v = qcore.ScopedValue("a") 29 | 30 | 31 | def test_true_fn(): 32 | assert_is(True, qcore.true_fn()) 33 | 34 | 35 | def test_false_fn(): 36 | assert_is(False, qcore.false_fn()) 37 | 38 | 39 | def test(): 40 | def nested(): 41 | assert_eq(v.get(), "b") 42 | with v.override("c"): 43 | assert_eq(v.get(), "c") 44 | 45 | assert_eq(v.get(), "a") 46 | with v.override("b"): 47 | assert_eq(v.get(), "b") 48 | nested() 49 | 50 | 51 | def test_exception(): 52 | assert_eq(v(), "a") 53 | try: 54 | with v.override("b"): 55 | assert_eq(v(), "b") 56 | raise NotImplementedError() 57 | except NotImplementedError: 58 | pass 59 | assert_eq(v(), "a") 60 | 61 | 62 | def test_override(): 63 | class TestObject: 64 | def __init__(self): 65 | self.v = None 66 | 67 | o = TestObject() 68 | o.v = "a" 69 | 70 | with qcore.override(o, "v", "b"): 71 | assert_eq(o.v, "b") 72 | try: 73 | with qcore.override(o, "v", "c"): 74 | assert_eq(o.v, "c") 75 | raise NotImplementedError() 76 | except NotImplementedError: 77 | pass 78 | assert_eq(o.v, "b") 79 | assert_eq(o.v, "a") 80 | 81 | 82 | def test_dict_to_object(): 83 | d = {"a": 1, "b": 2} 84 | 85 | o = qcore.dict_to_object(d) 86 | assert_eq(o.__dict__, d) 87 | 88 | o = qcore.dict_to_object(d) 89 | assert_eq(o.a, 1) 90 | assert_eq(o.b, 2) 91 | 92 | 93 | def test_copy_public_attrs(): 94 | def f(): 95 | pass 96 | 97 | f.hi = 1 98 | f.hello = 2 99 | f._hi = 3 100 | f._hello = 4 101 | 102 | def g(): 103 | pass 104 | 105 | g.hello = 5 106 | g._hi = 6 107 | g._hello = 7 108 | 109 | with AssertRaises(AttributeError): 110 | assert g.hi 111 | assert_eq(5, g.hello) 112 | assert_eq(6, g._hi) 113 | assert_eq(7, g._hello) 114 | 115 | qcore.copy_public_attrs(f, g) 116 | 117 | assert_eq(1, g.hi) 118 | assert_eq(2, g.hello) 119 | assert_eq(6, g._hi) 120 | assert_eq(7, g._hello) 121 | assert_ne(g.__code__, f.__code__) 122 | assert_ne(g.__name__, f.__name__) 123 | 124 | class A: 125 | pass 126 | 127 | a1 = A() 128 | a1._hey = 0 129 | a1.hi = 1 130 | a1.hello = 2 131 | a1._hi = 3 132 | a1._hello = 4 133 | 134 | a2 = A() 135 | a2.hello = 5 136 | a2._hi = 6 137 | a2._hello = 7 138 | 139 | assert_eq(0, a1._hey) 140 | assert_eq(1, a1.hi) 141 | assert_eq(2, a1.hello) 142 | assert_eq(3, a1._hi) 143 | assert_eq(4, a1._hello) 144 | with AssertRaises(AttributeError): 145 | assert a2.hi 146 | assert_eq(5, a2.hello) 147 | assert_eq(6, a2._hi) 148 | assert_eq(7, a2._hello) 149 | 150 | qcore.copy_public_attrs(a1, a2) 151 | 152 | assert_eq(0, a1._hey) 153 | assert_eq(1, a1.hi) 154 | assert_eq(2, a1.hello) 155 | assert_eq(3, a1._hi) 156 | assert_eq(4, a1._hello) 157 | with AssertRaises(AttributeError): 158 | assert a2._hey 159 | assert_eq(1, a2.hi) 160 | assert_eq(2, a2.hello) 161 | assert_eq(6, a2._hi) 162 | assert_eq(7, a2._hello) 163 | 164 | 165 | def test_cached_hash_wrapper(): 166 | class TestClass: 167 | pass 168 | 169 | w1a = qcore.CachedHashWrapper(TestClass()) 170 | w1b = qcore.CachedHashWrapper(w1a()) 171 | w2a = qcore.CachedHashWrapper(TestClass()) 172 | 173 | print("w1a", w1a) 174 | print("w1b", w1b) 175 | print("w2a", w2a) 176 | 177 | assert_is(w1a.value(), w1a()) 178 | assert_is(w1a(), w1b()) 179 | assert_is_not(w1a(), w2a()) 180 | 181 | assert_eq(w1a, w1b) 182 | assert_ne(w1a, w2a) 183 | assert_ne(w1b, w2a) 184 | 185 | assert_eq(w1a, w1b()) 186 | assert_ne(w1a, w2a()) 187 | assert_ne(w1b, w2a()) 188 | 189 | assert_eq(hash(w1a), hash(w1b)) 190 | assert_ne(hash(w1a), hash(w2a)) 191 | assert_ne(hash(w1b), hash(w2a)) 192 | 193 | 194 | def _stub_serializable_func(): 195 | pass 196 | 197 | 198 | def test_object_from_string(): 199 | def check(name, expected): 200 | actual = qcore.object_from_string(name) 201 | assert_eq(expected, actual) 202 | name = name.encode("ascii") 203 | with AssertRaises(TypeError): 204 | qcore.object_from_string(name) 205 | 206 | with AssertRaises(ValueError): 207 | # Not a fully qualified name 208 | qcore.object_from_string("FooBar") 209 | 210 | check("test_helpers._stub_serializable_func", _stub_serializable_func) 211 | import socket 212 | 213 | check("socket.gethostname", socket.gethostname) 214 | 215 | with AssertRaises(TypeError): 216 | # invalid type 217 | qcore.object_from_string({"name": "socket.gethostname"}) 218 | 219 | # test the case when the from import fails 220 | check("test_helpers.asserts_.assert_eq", assert_eq) 221 | 222 | 223 | def test_catchable_exceptions(): 224 | assert_is(True, qcore.catchable_exceptions(Exception)) 225 | assert_is(True, qcore.catchable_exceptions(BaseException)) 226 | assert_is(False, qcore.catchable_exceptions(tuple())) 227 | assert_is(True, qcore.catchable_exceptions((Exception,))) 228 | assert_is(False, qcore.catchable_exceptions((Exception, int))) 229 | assert_is(False, qcore.catchable_exceptions([Exception])) 230 | 231 | 232 | def test_ellipsis(): 233 | assert_eq("abcdef", qcore.ellipsis("abcdef", 0)) 234 | assert_eq("abcdef", qcore.ellipsis("abcdef", 10)) 235 | assert_eq("ab...", qcore.ellipsis("abcdef", 5)) 236 | 237 | 238 | def test_safe_representation(): 239 | class TestObject: 240 | """A test object that has neither __str__ nor __repr__.""" 241 | 242 | def __str__(self): 243 | return NotImplementedError() 244 | 245 | def __repr__(self): 246 | return NotImplementedError() 247 | 248 | assert_eq("2", qcore.safe_str(2)) 249 | assert_eq("2....", qcore.safe_str("2.192842", max_length=5)) 250 | assert_is_substring(" None: 92 | pass 93 | 94 | 95 | def fun_with_kwonly_args(a=1, *, b, c=3): 96 | pass 97 | 98 | 99 | def test_getargspec_py3_only(): 100 | spec = qcore.inspection.ArgSpec( 101 | args=["a", "b"], varargs="args", keywords=None, defaults=None 102 | ) 103 | assert_eq(spec, qcore.inspection.getargspec(fun_with_annotations)) 104 | with AssertRaises(ValueError): 105 | qcore.inspection.getargspec(fun_with_kwonly_args) 106 | 107 | 108 | class X: 109 | @classmethod 110 | def myclassmethod(cls): 111 | pass 112 | 113 | def myinstancemethod(self): 114 | pass 115 | 116 | 117 | class OldStyle: 118 | @classmethod 119 | def myclassmethod(cls): 120 | pass 121 | 122 | def myinstancemethod(self): 123 | pass 124 | 125 | 126 | class BoolConversionFails: 127 | def method(self): 128 | pass 129 | 130 | def __nonzero__(self): 131 | raise TypeError("Cannot convert %s to bool" % self) 132 | 133 | __bool__ = __nonzero__ 134 | 135 | 136 | def test_is_classmethod(): 137 | assert not qcore.inspection.is_classmethod(X) 138 | assert not qcore.inspection.is_classmethod(X()) 139 | assert not qcore.inspection.is_classmethod(OldStyle) 140 | assert not qcore.inspection.is_classmethod(OldStyle()) 141 | assert not qcore.inspection.is_classmethod("x") 142 | assert not qcore.inspection.is_classmethod(qcore) 143 | assert not qcore.inspection.is_classmethod(qcore.inspection.is_classmethod) 144 | assert qcore.inspection.is_classmethod(X.myclassmethod) 145 | assert qcore.inspection.is_classmethod(X().myclassmethod) 146 | assert qcore.inspection.is_classmethod(OldStyle.myclassmethod) 147 | assert qcore.inspection.is_classmethod(OldStyle().myclassmethod) 148 | assert not qcore.inspection.is_classmethod(X.myinstancemethod) 149 | assert not qcore.inspection.is_classmethod(X().myinstancemethod) 150 | assert not qcore.inspection.is_classmethod(OldStyle.myinstancemethod) 151 | assert not qcore.inspection.is_classmethod(OldStyle().myinstancemethod) 152 | # this throws an error if you do "not im_self" 153 | assert not qcore.inspection.is_classmethod(BoolConversionFails().method) 154 | 155 | 156 | def test_get_function_call_str(): 157 | class TestObject: 158 | """A test object containing no __str__ implementation.""" 159 | 160 | def __str__(self): 161 | raise NotImplementedError() 162 | 163 | def __repr__(self): 164 | return "test" 165 | 166 | def test_function(): 167 | pass 168 | 169 | function_str_kv = qcore.inspection.get_function_call_str( 170 | test_function, (1, 2, 3), {"k": "v"} 171 | ) 172 | function_str_dummy = qcore.inspection.get_function_call_str( 173 | test_function, (TestObject(),), {} 174 | ) 175 | 176 | assert_eq("test_inspection.test_function(1,2,3,k=v)", function_str_kv) 177 | assert_eq("test_inspection.test_function(test)", function_str_dummy) 178 | 179 | 180 | def test_get_function_call_repr(): 181 | def dummy_function(): 182 | pass 183 | 184 | function_repr_kv = qcore.inspection.get_function_call_repr( 185 | dummy_function, ("x",), {"k": "v"} 186 | ) 187 | function_repr_kr = qcore.inspection.get_function_call_repr( 188 | dummy_function, ("x",), {"k": "r"} 189 | ) 190 | 191 | assert_eq("test_inspection.dummy_function('x',k='v')", function_repr_kv) 192 | assert_ne(function_repr_kv, function_repr_kr) 193 | -------------------------------------------------------------------------------- /qcore/tests/test_microtime.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import time 17 | from datetime import datetime, timezone, timedelta 18 | 19 | import qcore 20 | from qcore.asserts import AssertRaises, assert_eq, assert_ne, assert_le 21 | 22 | from qcore.microtime import utime_delta, execute_with_timeout, TimeOffset 23 | 24 | 25 | def test_utime_delta_combines_dates_hours_minutes_and_seconds(): 26 | delta = utime_delta(days=1, hours=1, minutes=1, seconds=1) 27 | assert_eq(qcore.DAY + qcore.HOUR + qcore.MINUTE + qcore.SECOND, delta) 28 | 29 | 30 | def test_utime_delta_does_not_allow_positional_arguments(): 31 | with AssertRaises(TypeError): 32 | utime_delta(1, 2, seconds=3) 33 | 34 | 35 | def test_time_offset(): 36 | time_before_offset = qcore.utime() 37 | with TimeOffset(qcore.HOUR): 38 | time_during_offset = qcore.utime() 39 | time_after_offset = qcore.utime() 40 | 41 | assert_eq(time_before_offset, time_after_offset, tolerance=qcore.MINUTE) 42 | assert_ne(time_before_offset, time_during_offset, tolerance=qcore.MINUTE) 43 | assert_le(time_after_offset, time_during_offset) 44 | 45 | 46 | # =================================================== 47 | # Conversions to/from PY Date-Time 48 | # =================================================== 49 | 50 | 51 | PLUS_7_TZ = timezone(timedelta(hours=7)) 52 | 53 | 54 | def test_utime_as_datetime(): 55 | the_utime = 1667239323_123456 56 | 57 | # No options... 58 | actual_dt1 = qcore.utime_as_datetime(the_utime) 59 | # Defaults to UTC. 60 | assert_eq(actual_dt1.tzname(), "UTC") 61 | assert_eq(actual_dt1, datetime(2022, 10, 31, 18, 2, 3, 123456, tzinfo=timezone.utc)) 62 | 63 | # With tz... 64 | actual_dt2 = qcore.utime_as_datetime(the_utime, tz=PLUS_7_TZ) 65 | # Does have the timezone set. 66 | assert_eq(actual_dt2.tzname(), "UTC+07:00") 67 | assert_eq(actual_dt2, datetime(2022, 11, 1, 1, 2, 3, 123456, tzinfo=PLUS_7_TZ)) 68 | # But still equivalent to the UTC-timezoned value. 69 | assert_eq(actual_dt2, actual_dt1) 70 | assert_eq(actual_dt2, datetime(2022, 10, 31, 18, 2, 3, 123456, tzinfo=timezone.utc)) 71 | 72 | 73 | def test_datetime_as_utime(): 74 | the_utime = 1667239323_123456 75 | 76 | assert_eq( 77 | qcore.datetime_as_utime( 78 | datetime(2022, 10, 31, 18, 2, 3, 123456, tzinfo=timezone.utc) 79 | ), 80 | the_utime, 81 | ) 82 | 83 | assert_eq( 84 | qcore.datetime_as_utime( 85 | datetime(2022, 11, 1, 1, 2, 3, 123456, tzinfo=PLUS_7_TZ) 86 | ), 87 | the_utime, 88 | ) 89 | 90 | 91 | # =================================================== 92 | # Conversions to/from ISO 8601 Date-Time 93 | # =================================================== 94 | 95 | 96 | def test_format_utime_as_iso_8601(): 97 | the_utime = 1667239323_123456 98 | 99 | # No options... 100 | assert_eq( 101 | "2022-10-31T18:02:03.123456+00:00", qcore.format_utime_as_iso_8601(the_utime) 102 | ) 103 | 104 | # Separator... 105 | assert_eq( 106 | "2022-10-31 18:02:03.123456+00:00", 107 | qcore.format_utime_as_iso_8601(the_utime, sep=" "), 108 | ) 109 | assert_eq( 110 | "2022-10-31_18:02:03.123456+00:00", 111 | qcore.format_utime_as_iso_8601(the_utime, sep="_"), 112 | ) 113 | 114 | # Drop sub-seconds... 115 | assert_eq( 116 | "2022-10-31T18:02:03+00:00", 117 | qcore.format_utime_as_iso_8601(the_utime, drop_subseconds=True), 118 | ) 119 | 120 | # With tz... 121 | plus7_tz = timezone(timedelta(hours=7)) 122 | assert_eq( 123 | "2022-11-01T01:02:03.123456+07:00", 124 | qcore.format_utime_as_iso_8601(the_utime, tz=plus7_tz), 125 | ) 126 | 127 | 128 | if hasattr(qcore, "iso_8601_as_utime"): 129 | 130 | def test_iso_8601_as_utime(): 131 | the_utime = 1667239323_123456 132 | the_utime_at_second = 1667239323_000000 133 | 134 | assert_eq( 135 | the_utime, qcore.iso_8601_as_utime("2022-10-31T18:02:03.123456+00:00") 136 | ) 137 | assert_eq( 138 | the_utime, qcore.iso_8601_as_utime("2022-11-01T01:02:03.123456+07:00") 139 | ) 140 | 141 | assert_eq( 142 | the_utime_at_second, qcore.iso_8601_as_utime("2022-10-31T18:02:03+00:00") 143 | ) 144 | assert_eq( 145 | the_utime_at_second, qcore.iso_8601_as_utime("2022-11-01T01:02:03+07:00") 146 | ) 147 | 148 | 149 | # =================================================== 150 | # Timeout API 151 | # =================================================== 152 | 153 | 154 | def test_execute_with_timeout(): 155 | def run_forever(): 156 | while True: 157 | time.sleep(0.1) 158 | 159 | def run_quickly(): 160 | pass 161 | 162 | with AssertRaises(qcore.TimeoutError): 163 | execute_with_timeout(run_forever, timeout=0.2) 164 | execute_with_timeout(run_quickly, timeout=None) 165 | -------------------------------------------------------------------------------- /qcore/tests/test_testing.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from unittest.case import SkipTest 16 | 17 | from qcore.asserts import assert_eq, assert_is, AssertRaises 18 | from qcore.testing import ( 19 | Anything, 20 | decorate_all_test_methods, 21 | decorate_func_or_method_or_class, 22 | GreaterEq, 23 | disabled, 24 | TEST_PREFIX, 25 | ) 26 | 27 | 28 | def test_Anything(): 29 | assert_eq(Anything, None) 30 | assert_eq(Anything, []) 31 | assert_eq(None, Anything) 32 | assert_eq([], Anything) 33 | assert not (Anything != None) 34 | assert not (Anything != []) 35 | assert not (None != Anything) 36 | assert not ([] != Anything) 37 | assert_eq("", repr(Anything)) 38 | 39 | 40 | def test_GreaterEq(): 41 | assert_eq(GreaterEq(2), 3) 42 | assert_eq(GreaterEq(2), 2) 43 | assert not GreaterEq(2) != 3 44 | assert not GreaterEq(2) != 2 45 | 46 | with AssertRaises(AssertionError): 47 | assert_eq(GreaterEq(3), 2) 48 | 49 | assert_eq("", repr(GreaterEq(3))) 50 | 51 | 52 | def _check_disabled(fn): 53 | with AssertRaises(SkipTest): 54 | fn() 55 | 56 | 57 | def test_disabled(): 58 | @disabled 59 | def fn(): 60 | pass 61 | 62 | _check_disabled(fn) 63 | 64 | @disabled 65 | class TestCls: 66 | def test_method(self): 67 | return marker 68 | 69 | def normal_method(self): 70 | return marker 71 | 72 | _check_disabled(TestCls().test_method) 73 | assert_is(marker, TestCls().normal_method()) 74 | 75 | class TestCls2: 76 | def test_method(self): 77 | return marker 78 | 79 | @disabled 80 | def test_method_disabled(self): 81 | return marker 82 | 83 | def normal_method(self): 84 | return marker 85 | 86 | assert_is(marker, TestCls2().test_method()) 87 | _check_disabled(TestCls2().test_method_disabled) 88 | assert_is(marker, TestCls2().normal_method()) 89 | 90 | with AssertRaises(AssertionError): 91 | disabled(None) 92 | 93 | 94 | def normal_method(self): 95 | pass 96 | 97 | 98 | marker = object() 99 | test_method_name = TEST_PREFIX + "_method" 100 | test_member_name = TEST_PREFIX + "_member" 101 | 102 | 103 | def decorator(method): 104 | return marker 105 | 106 | 107 | def _get_decoratable_class(): 108 | class Cls: 109 | pass 110 | 111 | Cls.normal_method = normal_method 112 | 113 | test_method = lambda self: None 114 | setattr(Cls, test_method_name, test_method) 115 | assert_eq(test_method.__get__(None, Cls), getattr(Cls, test_method_name)) 116 | 117 | setattr(Cls, test_member_name, "not a method") 118 | return Cls 119 | 120 | 121 | def _assert_is_decorated(new_cls, cls): 122 | assert_is(new_cls, cls) 123 | assert_eq(normal_method.__get__(None, new_cls), new_cls.normal_method) 124 | assert_is(marker, getattr(new_cls, test_method_name)) 125 | assert_eq("not a method", getattr(new_cls, test_member_name)) 126 | 127 | 128 | def test_decorate_all_test_methods(): 129 | cls = _get_decoratable_class() 130 | new_cls = decorate_all_test_methods(decorator)(cls) 131 | _assert_is_decorated(new_cls, cls) 132 | 133 | 134 | def test_decorate_func_or_method_or_class(): 135 | cls = _get_decoratable_class() 136 | new_cls = decorate_func_or_method_or_class(decorator)(cls) 137 | _assert_is_decorated(new_cls, cls) 138 | 139 | assert_is(marker, decorate_func_or_method_or_class(decorator)(normal_method)) 140 | 141 | with AssertRaises(AssertionError): 142 | decorate_func_or_method_or_class(decorator)(None) 143 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==8.3.3 2 | mypy==1.11.2 3 | black==24.10.0 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Quora, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from setuptools import setup 16 | from setuptools.extension import Extension 17 | 18 | import glob 19 | import os.path 20 | 21 | 22 | CYTHON_MODULES = [ 23 | "helpers", 24 | "microtime", 25 | "events", 26 | "decorators", 27 | "caching", 28 | "inspection", 29 | ] 30 | 31 | 32 | DATA_FILES = ( 33 | ["py.typed"] 34 | + ["%s.pxd" % module for module in CYTHON_MODULES] 35 | + [os.path.relpath(f, "qcore/") for f in glob.glob("qcore/*.pyi")] 36 | ) 37 | 38 | 39 | VERSION = "1.11.0" 40 | 41 | 42 | EXTENSIONS = [ 43 | Extension("qcore.%s" % module, ["qcore/%s.py" % module]) 44 | for module in CYTHON_MODULES 45 | ] 46 | 47 | 48 | if __name__ == "__main__": 49 | for extension in EXTENSIONS: 50 | extension.cython_directives = {"language_level": "3"} 51 | 52 | with open("./README.rst", encoding="utf-8") as f: 53 | long_description = f.read() 54 | 55 | setup( 56 | name="qcore", 57 | version=VERSION, 58 | description="Quora's core utility library", 59 | long_description=long_description, 60 | long_description_content_type="text/x-rst", 61 | url="https://github.com/quora/qcore", 62 | packages=["qcore", "qcore.tests"], 63 | package_data={"qcore": DATA_FILES}, 64 | ext_modules=EXTENSIONS, 65 | ) 66 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion=2.3.1 3 | envlist = 4 | py39,py310,py311,py312,py313 5 | mypy 6 | black 7 | skip_missing_interpreters = True 8 | 9 | [testenv] 10 | deps = 11 | -rrequirements.txt 12 | 13 | commands = 14 | python -X dev -Werror -m pytest qcore 15 | 16 | [testenv:mypy] 17 | basepython = python3.9 18 | deps = 19 | -rrequirements.txt 20 | 21 | commands = 22 | mypy qcore 23 | 24 | [testenv:black] 25 | commands = 26 | black --check . 27 | 28 | [gh-actions] 29 | python = 30 | 3.9: py39, mypy 31 | 3.10: py310 32 | 3.11: py311 33 | 3.12: py312, black 34 | 3.13: py313 35 | --------------------------------------------------------------------------------