├── .github └── workflows │ ├── coverage.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── big ├── __init__.py ├── all.py ├── boundinnerclass.py ├── builtin.py ├── deprecated.py ├── file.py ├── graph.py ├── heap.py ├── itertools.py ├── log.py ├── metadata.py ├── scheduler.py ├── state.py ├── text.py ├── time.py └── version.py ├── pyproject.toml ├── resources ├── experiments │ ├── alice.in.wonderland.txt │ └── time_multisplit.py ├── images │ ├── big.header.png │ └── big.package.final.3.xcf └── unicode │ └── parse_ucd_xml.py └── tests ├── bigtestlib.py ├── grepfile ├── test_all.py ├── test_boundinnerclass.py ├── test_builtin.py ├── test_deprecated.py ├── test_encodings ├── ascii_source_code_encoding.py ├── gb18030_bom.py ├── invalid_conflicting_bom_and_source_code_encoding.py ├── invalid_source_code_encoding.py ├── invalid_utf-1_bom.py ├── utf-7_bom.py ├── utf-7_source_code_encoding.py ├── utf-8_source_code_encoding.py └── utf16_bom.py ├── test_file.py ├── test_graph.py ├── test_heap.py ├── test_itertools.py ├── test_log.py ├── test_metadata.py ├── test_scheduler.py ├── test_search_path ├── empty_path │ └── this_file_doesnt_match_anything ├── file_without_extension │ └── foobar ├── foo_d_path │ └── foo.d │ │ └── file.txt ├── foo_h_path │ ├── foo.h │ └── foo.hpp └── want_directories │ └── mydir │ └── file ├── test_state.py ├── test_text.py ├── test_time.py └── test_version.py /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Run coverage 2 | 3 | on: 4 | push: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | run-coverage: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python 16 | uses: actions/setup-python@v3 17 | with: 18 | python-version: '3.x' 19 | - name: Install necessary dependencies 20 | run: | 21 | python -m pip install --upgrade pip coverage 22 | pip install . 23 | - name: Run coverage without optional dependencies 24 | run: | 25 | coverage run --parallel-mode tests/test_all.py 26 | - name: Install optional dependencies 27 | run: | 28 | python -m pip install . '.[test]' 29 | - name: Run coverage with optional dependencies 30 | run: | 31 | coverage run --parallel-mode tests/test_all.py 32 | - name: Produce coverage reports 33 | run: | 34 | coverage combine 35 | coverage html -i 36 | echo '```' >> $GITHUB_STEP_SUMMARY 37 | coverage report -i >> $GITHUB_STEP_SUMMARY 38 | echo '```' >> $GITHUB_STEP_SUMMARY 39 | - name: Upload htmlcov 40 | uses: actions/upload-artifact@v3 41 | with: 42 | name: big_htmlcov 43 | path: htmlcov/ 44 | - name: Fail if not 100% coverage 45 | run: | 46 | coverage report -i --fail-under=100 47 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | build-wheel: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python 16 | uses: actions/setup-python@v3 17 | with: 18 | python-version: '3.x' 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade flit pip 22 | - name: Build wheel 23 | run: | 24 | flit build --format wheel 25 | - name: Upload wheel 26 | uses: actions/upload-artifact@v3 27 | with: 28 | name: wheel 29 | path: dist/*.whl 30 | 31 | test: 32 | needs: [build-wheel] 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 37 | 38 | runs-on: ubuntu-20.04 39 | 40 | steps: 41 | - uses: actions/checkout@v3 42 | - name: Set up Python 43 | uses: actions/setup-python@v3 44 | with: 45 | python-version: '${{ matrix.version }}' 46 | - name: Download wheel 47 | uses: actions/download-artifact@v3 48 | with: 49 | name: wheel 50 | path: ./ 51 | - name: Install wheel 52 | run: | 53 | pip install *.whl 54 | - name: Run tests without optional dependencies 55 | run: | 56 | python tests/test_all.py 57 | - name: Install optional dependencies 58 | run: | 59 | python -m pip install . '.[test]' 60 | - name: Run tests with optional dependencies 61 | run: | 62 | python tests/test_all.py 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build 3 | dist 4 | .coverage 5 | htmlcov 6 | todo.txt 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | big 2 | Copyright 2022-2024 Larry Hastings 3 | All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 21 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /big/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | The big package is a grab-bag of cool code for use in your programs. 5 | 6 | Think big! 7 | """ 8 | 9 | _license = """ 10 | big 11 | Copyright 2022-2024 Larry Hastings 12 | All rights reserved. 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a 15 | copy of this software and associated documentation files (the "Software"), 16 | to deal in the Software without restriction, including without limitation 17 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 18 | and/or sell copies of the Software, and to permit persons to whom the 19 | Software is furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included 22 | in all copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 25 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 26 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 27 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 28 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 29 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 30 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | """ 32 | 33 | __version__ = "0.12.8" 34 | -------------------------------------------------------------------------------- /big/all.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | The big package is a grab-bag of cool code for use in your programs. 5 | 6 | Think big! 7 | """ 8 | 9 | _license = """ 10 | big 11 | Copyright 2022-2024 Larry Hastings 12 | All rights reserved. 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a 15 | copy of this software and associated documentation files (the "Software"), 16 | to deal in the Software without restriction, including without limitation 17 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 18 | and/or sell copies of the Software, and to permit persons to whom the 19 | Software is furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included 22 | in all copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 25 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 26 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 27 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 28 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 29 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 30 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | """ 32 | 33 | 34 | import big 35 | 36 | __all__ = ['_license'] 37 | 38 | from . import boundinnerclass 39 | __all__.extend(boundinnerclass.__all__) 40 | from .boundinnerclass import * 41 | 42 | from . import builtin 43 | __all__.extend(builtin.__all__) 44 | from .builtin import * 45 | 46 | from . import deprecated 47 | # we DON'T splat all the symbols from deprecated into big.all! 48 | 49 | from . import file 50 | __all__.extend(file.__all__) 51 | from .file import * 52 | 53 | from . import graph 54 | __all__.extend(graph.__all__) 55 | from .graph import * 56 | 57 | from . import heap 58 | __all__.extend(heap.__all__) 59 | from .heap import * 60 | 61 | from . import itertools 62 | __all__.extend(itertools.__all__) 63 | from .itertools import * 64 | 65 | from . import log 66 | __all__.extend(log.__all__) 67 | from .log import * 68 | 69 | from . import metadata 70 | # we DON'T splat all symbols from metadata into big.all, either. 71 | 72 | from . import scheduler 73 | __all__.extend(scheduler.__all__) 74 | from .scheduler import * 75 | 76 | from . import state 77 | __all__.extend(state.__all__) 78 | from .state import * 79 | 80 | from . import text 81 | __all__.extend(text.__all__) 82 | from .text import * 83 | 84 | from . import time 85 | __all__.extend(time.__all__) 86 | from .time import * 87 | 88 | from . import version 89 | __all__.extend(version.__all__) 90 | from .version import * 91 | 92 | __version__ = big.__version__ 93 | __all__.append('__version__') 94 | 95 | -------------------------------------------------------------------------------- /big/boundinnerclass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | _license = """ 4 | big 5 | Copyright 2022-2024 Larry Hastings 6 | All rights reserved. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a 9 | copy of this software and associated documentation files (the "Software"), 10 | to deal in the Software without restriction, including without limitation 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 12 | and/or sell copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included 16 | in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | """ 26 | 27 | # 28 | # If you have a class with an inner class inside, 29 | # decorate the inner class with BoundInnerClass below 30 | # and now it'll automatically get a second parameter 31 | # of the parent instance! 32 | # _______________________________________ 33 | # 34 | # class Outer: 35 | # @BoundInnerClass 36 | # class Inner: 37 | # def __init__(self, outer): 38 | # global o 39 | # print(outer = o) 40 | # o = Outer() 41 | # i = o.Inner() 42 | # _______________________________________ 43 | # 44 | # This program prints True. The "outer" parameter 45 | # to Inner.__init__ was filled in automatically by 46 | # the BoundInnerClass decorator. 47 | # 48 | # Thanks, BoundInnerClass, you've saved the day! 49 | # 50 | # ----- 51 | # 52 | # Infinite extra-special thanks to Alex Martelli for 53 | # showing me how this could be done in the first place-- 54 | # all the way back in 2010! 55 | # 56 | # https://stackoverflow.com/questions/2278426/inner-classes-how-can-i-get-the-outer-class-object-at-construction-time 57 | # 58 | # Thanks, Alex, you've saved the day! 59 | 60 | 61 | __all__ = ["BoundInnerClass", "UnboundInnerClass"] 62 | 63 | import weakref 64 | 65 | class _Worker(object): 66 | def __init__(self, cls): 67 | self.cls = cls 68 | 69 | def __get__(self, outer, outer_class): 70 | 71 | # 72 | # If outer is not None, we've been accessed through 73 | # an *instance* of the class, like so: 74 | # o = Outer() 75 | # ref = o.Inner 76 | # 77 | # See: 78 | # https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance 79 | # 80 | # 81 | # If outer is None, we've been accessed through the *class itself*, 82 | # like so: 83 | # ref = Outer.Inner 84 | # 85 | # See: 86 | # https://docs.python.org/3/howto/descriptor.html#invocation-from-a-class 87 | # 88 | # If someone accesses us through the class, not through an instance, 89 | # return the unbound version of the class. 90 | # 91 | if outer is None: 92 | return self.cls 93 | 94 | name = self.cls.__name__ 95 | 96 | wrapper_bases = [self.cls] 97 | 98 | # Iterate over cls's bases, and look in outer, 99 | # to see if any have bound inner classes in outer. 100 | # If so, multiply inherit from the bound inner version(s). 101 | 102 | multiply_inherit = False 103 | for base in self.cls.__bases__: 104 | # If we can't find a bound inner base, 105 | # add the original unbound base instead. 106 | # this is harmless but helps preserve the original MRO. 107 | inherit_from = base 108 | 109 | # If we inherit from a BoundInnerClass from another outer class, 110 | # we *might* have the same name as a legitimate base class. 111 | # But if we get that attribute, we'll call __get__ recursively... 112 | # forever. 113 | # 114 | # If the name is the same as our own name, there's no way it's 115 | # a bound inner class we need to inherit from, so just skip it. 116 | if base.__name__ != name: 117 | bound_inner_base = getattr(outer, base.__name__, None) 118 | if bound_inner_base: 119 | bases = getattr(bound_inner_base, "__bases__", (None,)) 120 | # The unbound class is always the first base of 121 | # the bound inner class. 122 | if bases[0] == base: 123 | inherit_from = bound_inner_base 124 | multiply_inherit = True 125 | wrapper_bases.append(inherit_from) 126 | 127 | Wrapper = self._wrap(outer, wrapper_bases[0]) 128 | Wrapper.__name__ = name 129 | 130 | # Assigning to __bases__ is startling, but it's the only way to get 131 | # this code working simultaneously in both Python 2 and Python 3. 132 | if multiply_inherit: 133 | Wrapper.__bases__ = tuple(wrapper_bases) 134 | 135 | # Cache the bound instance in outer's dict. 136 | # This also means that future requests for us won't call our 137 | # __get__ again, it'll automatically use the cached copy. 138 | outer.__dict__[name] = Wrapper 139 | return Wrapper 140 | 141 | 142 | class BoundInnerClass(_Worker): 143 | """ 144 | Class decorator for an inner class. When accessing the inner class 145 | through an instance of the outer class, "binds" the inner class to 146 | the instance. This changes the signature of the inner class's __init__ 147 | from 148 | 149 | def __init__(self, *args, **kwargs): 150 | 151 | to 152 | 153 | def __init__(self, outer, *args, **kwargs): 154 | 155 | where "outer" is the instance of the outer class. 156 | 157 | Note that this has an implication for all subclasses. 158 | If class B is decorated with BoundInnerClass, and class 159 | S is a subclass of B, such that issubclass(S, B) returns True, 160 | class S must be decorated with either BoundInnerClass 161 | or UnboundInnerClass. 162 | """ 163 | def _wrap(self, outer, base): 164 | wrapper_self = self 165 | assert outer is not None 166 | outer_weakref = weakref.ref(outer) 167 | class Wrapper(base): 168 | def __init__(self, *bic_args, **bic_kwargs): 169 | wrapper_self.cls.__init__(self, 170 | outer_weakref(), *bic_args, **bic_kwargs) 171 | 172 | # give the bound inner class a nicer repr 173 | # (but only if it doesn't already have a custom repr) 174 | if wrapper_self.cls.__repr__ is object.__repr__: 175 | def __repr__(self): 176 | return "".join([ 177 | "<", 178 | wrapper_self.cls.__module__, 179 | ".", 180 | self.__class__.__name__, 181 | " object bound to ", 182 | repr(outer_weakref()), 183 | " at ", 184 | hex(id(self)), 185 | ">"]) 186 | Wrapper.__name__ = self.cls.__name__ 187 | Wrapper.__module__ = self.cls.__module__ 188 | Wrapper.__qualname__ = self.cls.__qualname__ 189 | Wrapper.__doc__ = self.cls.__doc__ 190 | if hasattr(self.cls, '__annotations__'): 191 | Wrapper.__annotations__ = self.cls.__annotations__ 192 | return Wrapper 193 | 194 | 195 | class UnboundInnerClass(_Worker): 196 | """ 197 | Class decorator for an inner class that prevents binding 198 | the inner class to an instance of the outer class. 199 | 200 | If class B is decorated with BoundInnerClass, and class 201 | S is a subclass of B, such that issubclass(S, B) returns True, 202 | class S must be decorated with either BoundInnerClass 203 | or UnboundInnerClass. 204 | """ 205 | def _wrap(self, outer, base): 206 | class Wrapper(base): 207 | pass 208 | Wrapper.__name__ = self.cls.__name__ 209 | Wrapper.__module__ = self.cls.__module__ 210 | Wrapper.__qualname__ = self.cls.__qualname__ 211 | Wrapper.__doc__ = self.cls.__doc__ 212 | if hasattr(self.cls, '__annotations__'): 213 | Wrapper.__annotations__ = self.cls.__annotations__ 214 | return Wrapper 215 | -------------------------------------------------------------------------------- /big/builtin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | _license = """ 4 | big 5 | Copyright 2022-2024 Larry Hastings 6 | All rights reserved. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a 9 | copy of this software and associated documentation files (the "Software"), 10 | to deal in the Software without restriction, including without limitation 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 12 | and/or sell copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included 16 | in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | """ 26 | 27 | 28 | __all__ = [] 29 | 30 | def export(o): 31 | __all__.append(o.__name__) 32 | return o 33 | 34 | 35 | from functools import update_wrapper 36 | from inspect import signature 37 | 38 | from types import SimpleNamespace as namespace 39 | __all__.append('namespace') 40 | 41 | 42 | @export 43 | def try_float(o): 44 | """ 45 | Returns True if o can be converted into a float, 46 | and False if it can't. 47 | """ 48 | try: 49 | float(o) 50 | return True 51 | except (TypeError, ValueError): 52 | return False 53 | 54 | @export 55 | def try_int(o): 56 | """ 57 | Returns True if o can be converted into an int, 58 | and False if it can't. 59 | """ 60 | try: 61 | int(o) 62 | return True 63 | except (TypeError, ValueError): 64 | return False 65 | 66 | _sentinel = object() 67 | 68 | @export 69 | def get_float(o, default=_sentinel): 70 | """ 71 | Returns float(o), unless that conversion fails, 72 | in which case returns the default value. If 73 | you don't pass in an explicit default value, 74 | the default value is o. 75 | """ 76 | try: 77 | return float(o) 78 | except (TypeError, ValueError): 79 | if default != _sentinel: 80 | return default 81 | return o 82 | 83 | @export 84 | def get_int(o, default=_sentinel): 85 | """ 86 | Returns int(o), unless that conversion fails, 87 | in which case returns the default value. If 88 | you don't pass in an explicit default value, 89 | the default value is o. 90 | """ 91 | try: 92 | return int(o) 93 | except (TypeError, ValueError): 94 | if default != _sentinel: 95 | return default 96 | return o 97 | 98 | @export 99 | def get_int_or_float(o, default=_sentinel): 100 | """ 101 | Converts o into a number, preferring an int to a float. 102 | 103 | If o is already an int, return o unchanged. 104 | If o is already a float, if int(o) == o, return int(o), 105 | otherwise return o. 106 | Otherwise, tries int(o). If that conversion succeeds, 107 | returns the result. 108 | Failing that, tries float(o). If that conversion succeeds, 109 | returns the result. 110 | If all else fails, returns the default value. If you don't 111 | pass in an explicit default value, the default value is o. 112 | """ 113 | if isinstance(o, int): 114 | return o 115 | if isinstance(o, float): 116 | int_o = int(o) 117 | if int_o == o: 118 | return int_o 119 | return o 120 | try: 121 | return int(o) 122 | except (TypeError, ValueError): 123 | try: 124 | # if you pass in the *string* "0.0", 125 | # this will return 0, not 0.0. 126 | return get_int_or_float(float(o)) 127 | except (TypeError, ValueError): 128 | if default != _sentinel: 129 | return default 130 | return o 131 | 132 | @export 133 | def pure_virtual(): 134 | """ 135 | Decorator for pure virtual methods. Calling a method 136 | decorated with this raises a NotImplementedError exception. 137 | """ 138 | def pure_virtual(fn): 139 | def wrapper(self, *args, **kwargs): 140 | raise NotImplementedError(f"pure virtual method {fn.__name__} called") 141 | update_wrapper(wrapper, fn) 142 | wrapper.__signature__ = signature(fn) 143 | return wrapper 144 | return pure_virtual 145 | -------------------------------------------------------------------------------- /big/heap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | _license = """ 4 | big 5 | Copyright 2022-2024 Larry Hastings 6 | All rights reserved. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a 9 | copy of this software and associated documentation files (the "Software"), 10 | to deal in the Software without restriction, including without limitation 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 12 | and/or sell copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included 16 | in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | """ 26 | 27 | __all__ = ['Heap'] 28 | 29 | from heapq import ( 30 | heapify, 31 | heappop, 32 | heappush, 33 | heappushpop, 34 | heapreplace, 35 | nlargest, 36 | nsmallest, 37 | ) 38 | 39 | class Heap: 40 | """ 41 | An object-oriented wrapper around the heapq library, designed to be 42 | easy to use--and easy to remember how to use. 43 | The Heap API mimics the list and collections.deque objects; this way, 44 | all you need to remember is "it works kinda like a list object". 45 | 46 | Specifics: 47 | * Use Heap.append to add a value to the heap. 48 | * Use Heap.extend to add multiple values to the heap. 49 | * Use Heap.popleft to remove the first value from the heap. 50 | * Use Heap[0] to peek at the first value on the heap. 51 | """ 52 | def __init__(self, i=None): 53 | if not i: 54 | self._queue = [] 55 | else: 56 | self._queue = list(i) 57 | heapify(self._queue) 58 | self._version = 0 59 | 60 | def __repr__(self): 61 | id_string = hex(id(self))[-6:] 62 | return f"" 63 | 64 | def append(self, o): 65 | heappush(self._queue, o) 66 | self._version += 1 67 | 68 | def clear(self): 69 | self._queue.clear() 70 | self._version += 1 71 | 72 | def copy(self): 73 | copy = self.__class__() 74 | copy._queue = self._queue.copy() 75 | return copy 76 | 77 | def extend(self, i): 78 | self._queue.extend(i) 79 | heapify(self._queue) 80 | self._version += 1 81 | 82 | def remove(self, o): 83 | self._queue.remove(o) 84 | heapify(self._queue) 85 | self._version += 1 86 | 87 | def popleft(self): 88 | o = heappop(self._queue) 89 | self._version += 1 90 | return o 91 | 92 | def append_and_popleft(self, o): 93 | o = heappushpop(self._queue, o) 94 | self._version += 1 95 | return o 96 | 97 | def popleft_and_append(self, o): 98 | o = heapreplace(self._queue, o) 99 | self._version += 1 100 | return o 101 | 102 | @property 103 | def queue(self): 104 | queue = list(self._queue) 105 | queue.sort() 106 | return queue 107 | 108 | def __bool__(self): 109 | return bool(self._queue) 110 | 111 | def __contains__(self, o): 112 | return o in self._queue 113 | 114 | def __eq__(self, other): 115 | return isinstance(other, self.__class__) and (self.queue == other.queue) 116 | 117 | def __len__(self): 118 | return len(self._queue) 119 | 120 | def __getitem__(self, item): 121 | if not isinstance(item, (int, slice)): 122 | raise TypeError(f"heap only supports indexing with int and slice") 123 | if isinstance(item, slice): 124 | if item.step in (None, 1): 125 | length = len(self._queue) 126 | start = item.start or 0 127 | stop = item.stop if item.stop is not None else length 128 | if start < 0: 129 | start += length 130 | if stop < 0: 131 | stop += length 132 | if start == 0: 133 | return nsmallest(stop, self._queue) 134 | if stop == length: 135 | # nlargest returns the largest... IN REVERSE ORDER. *smh* 136 | result = nlargest(length - start, self._queue) 137 | result.reverse() 138 | return result 139 | # fall through! it'll work! 140 | elif item == 0: 141 | return self._queue[0] 142 | elif item == -1: 143 | return max(self._queue) 144 | elif 0 < item < 10: 145 | l = nsmallest(item + 1, self._queue) 146 | return l[item] 147 | elif -10 < item < 0: 148 | l = nlargest((-item) + 1, self._queue) 149 | return l[item] 150 | l = sorted(self._queue) 151 | # __getitem__ on a list handles ints and slices! 152 | return l[item] 153 | 154 | class HeapIterator: 155 | def __init__(self, heap): 156 | self._heap = heap 157 | self._copy = heap._queue.copy() 158 | self._version = heap._version 159 | 160 | def __next__(self): 161 | if self._version != self._heap._version: 162 | raise RuntimeError("heap modified during iteration") 163 | if not self._copy: 164 | raise StopIteration 165 | return heappop(self._copy) 166 | 167 | def __repr__(self): 168 | return f"" 169 | 170 | def __iter__(self): 171 | return self.HeapIterator(self) 172 | 173 | -------------------------------------------------------------------------------- /big/itertools.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | __all__ = ['PushbackIterator'] 4 | class PushbackIterator: 5 | """ 6 | Wraps any iterator, allowing you to push items back on the iterator. 7 | This allows you to "peek" at the next item (or items); you can get the 8 | next item, examine it, and then push it back. If any objects have 9 | been pushed onto the iterator, they are yielded first, before attempting 10 | to yield from the wrapped iterator. 11 | 12 | Pass in any iterable to the constructor. Passing in an iterable of None 13 | means the PushbackIterator is created in an exhausted state. 14 | 15 | When the wrapped iterable is exhausted (or if you passed in None to 16 | the constructor) you can still call push to add new items, at which 17 | point the PushBackIterator can be iterated over again. 18 | """ 19 | 20 | __slots__ = ('i', 'stack') 21 | 22 | def __init__(self, iterable=None): 23 | if (iterable != None) and (not hasattr(iterable, '__next__')): 24 | iterable = iter(iterable) 25 | self.i = iterable 26 | self.stack = [] 27 | 28 | def __iter__(self): 29 | return self 30 | 31 | def push(self, o): 32 | """ 33 | Pushes a value into the iterator's internal stack. 34 | When a PushbackIterator is iterated over, and there are 35 | any pushed values, the top value on the stack will be popped 36 | and yielded. PushbackIterator only yields from the 37 | iterator it wraps when this internal stack is empty. 38 | """ 39 | self.stack.append(o) 40 | 41 | def __next__(self): 42 | if self.stack: 43 | return self.stack.pop() 44 | if self.i is not None: 45 | try: 46 | return next(self.i) 47 | except: 48 | self.i = None 49 | raise StopIteration 50 | 51 | def next(self, default=None): 52 | """ 53 | Equivalent to next(PushbackIterator), 54 | but won't raise StopIteration. 55 | If the iterator is exhausted, returns 56 | the "default" argument. 57 | """ 58 | if self.stack: 59 | return self.stack.pop() 60 | if self.i is not None: 61 | try: 62 | return next(self.i) 63 | except StopIteration: 64 | self.i = None 65 | return default 66 | 67 | def __bool__(self): 68 | if self.stack: 69 | return True 70 | if self.i is not None: 71 | try: 72 | o = next(self.i) 73 | self.push(o) 74 | return True 75 | except StopIteration: 76 | self.i = None 77 | return False 78 | 79 | def __repr__(self): 80 | return f"<{self.__class__.__name__} i={self.i} stack={self.stack}>" 81 | 82 | -------------------------------------------------------------------------------- /big/log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | _license = """ 4 | big 5 | Copyright 2022-2024 Larry Hastings 6 | All rights reserved. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a 9 | copy of this software and associated documentation files (the "Software"), 10 | to deal in the Software without restriction, including without limitation 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 12 | and/or sell copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included 16 | in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | """ 26 | 27 | 28 | __all__ = ['default_clock', 'Log'] 29 | 30 | 31 | import builtins 32 | 33 | try: 34 | # new in 3.7 35 | from time import monotonic_ns as default_clock 36 | except ImportError: # pragma: no cover 37 | from time import perf_counter 38 | def default_clock(): 39 | return int(perf_counter() * 1000000000.0) 40 | 41 | 42 | class Log: 43 | """ 44 | A simple lightweight logging class, useful for performance analysis. 45 | Not intended as a full-fledged logging facility like Python's 46 | "logging" module. 47 | 48 | The clock named parameter specifies the function to call to get the 49 | time. This function should return an int, representing elapsed time 50 | in nanoseconds. 51 | 52 | Allows nesting, which is literally just a presentation thing. 53 | 54 | To use: first, create your Log object. 55 | log = Log() 56 | Then log events by calling your Log object, 57 | passing in a string describing the event. 58 | log('text') 59 | Enter a nested subsystem containing events with log.enter: 60 | log.enter('subsystem') 61 | Then later exit that subsystem with log.exit: 62 | log.exit() 63 | And finally print the log: 64 | log.print() 65 | 66 | You can also iterate over the log events using iter(log). 67 | This yields 4-tuples: 68 | (start_time, elapsed_time, event, depth) 69 | start_time and elapsed_time are times, expressed as 70 | an integer number of nanoseconds. The first event 71 | is at start_time 0, and all subsequent start times are 72 | relative to that time. event is the event string you 73 | passed in to log() (or " start" or 74 | " end"). depth is an integer indicating 75 | how many subsystems the event is nested in; larger 76 | numbers indicate deeper nesting. 77 | """ 78 | 79 | # Internally, all times are stored relative to 80 | # the logging start time. 81 | # Writing this any other way was *madness.* 82 | # (You're dealing with 15-digit numbers that change 83 | # randomly in the last... seven? digits. You'll go blind!) 84 | 85 | def __init__(self, clock=None): 86 | clock = clock or default_clock 87 | self.clock = clock 88 | self.reset() 89 | 90 | def reset(self): 91 | """ 92 | Resets the log to its initial state. 93 | After resetting the log, the log is 94 | empty except for the initial "log start" 95 | message, the elapsed time is zero, and 96 | the log has not "entered" any subsystems. 97 | """ 98 | self.start = self.clock() 99 | self.stack = [] 100 | 101 | event = 'log start' 102 | self.longest_event = len(event) 103 | self.events = [(0, event, 0)] 104 | 105 | def enter(self, subsystem): 106 | """ 107 | Notifies the log that you've entered a subsystem. 108 | The subsystem parameter should be a string 109 | describing the subsystem. 110 | 111 | This is really just a presentation thing; 112 | all subsequent logged entries will be indented 113 | until you make the corresponding `log.exit()` call. 114 | 115 | You may nest subsystems as deeply as you like. 116 | """ 117 | self(subsystem + " start") 118 | self.stack.append(subsystem) 119 | 120 | def exit(self): 121 | """ 122 | Exits a logged subsystem. See Log.enter(). 123 | """ 124 | subsystem = self.stack.pop() 125 | self(subsystem + " end") 126 | 127 | def __call__(self, event): 128 | """ 129 | Log an event. Pass in a string specifying the event. 130 | """ 131 | t = self.clock() - self.start 132 | self.events.append((t, event, len(self.stack))) 133 | self.end = t 134 | self.longest_event = max(self.longest_event, len(event)) 135 | 136 | def __iter__(self): 137 | """ 138 | Iterate over the events of the log. 139 | Yields 4-tuples: 140 | (start_time, elapsed_time, event, depth) 141 | start_time and elapsed_time are floats. 142 | event is the string you specified when you logged. 143 | depth is an integer; the higher the number, 144 | the more nested this event is (how many log.enter() 145 | calls are active). 146 | """ 147 | i = iter(tuple(self.events)) 148 | waiting = next(i) 149 | for e in i: 150 | t, event, depth = waiting 151 | next_t = e[0] 152 | yield (t, next_t - t, event, depth) 153 | waiting = e 154 | t, event, depth = waiting 155 | yield (t, 0, event, depth) 156 | 157 | def print(self, *, print=None, title="[event log]", headings=True, indent=2, seconds_width=2, fractional_width=9): 158 | """ 159 | Prints the log. 160 | 161 | Keyword-only parameters: 162 | 163 | print specifies the print function to use, default is builtins.print. 164 | title specifies the title to print at the beginning. 165 | Default is "[event log]". To suppress, pass in None. 166 | headings is a boolean; if True (the default), prints column headings 167 | for the log. 168 | indent is the number of spaces to indent in front of log entries, 169 | and also how many spaces to indent each time we enter a subsystem. 170 | seconds_width is how wide to make the seconds column, default 2. 171 | fractional_width is how wide to make the fractional column, default 9. 172 | """ 173 | if not print: 174 | print = builtins.print 175 | 176 | indent = " " * indent 177 | column_width = seconds_width + 1 + fractional_width 178 | 179 | def format_time(t): 180 | assert t is not None 181 | seconds = t // 1_000_000_000 182 | nanoseconds = f"{t - seconds:>09}" 183 | nanoseconds = nanoseconds[:fractional_width] 184 | return f"{seconds:0{seconds_width}}.{nanoseconds}" 185 | 186 | if title: 187 | print(title) 188 | 189 | if headings: 190 | print(f"{indent}{'start':{column_width}} {'elapsed':{column_width}} event") 191 | column_dashes = '-' * column_width 192 | print(f"{indent}{column_dashes} {column_dashes} {'-' * self.longest_event}") 193 | 194 | formatted = [] 195 | for start, elapsed, event, depth in self: 196 | indent2 = indent * depth 197 | print(f"{indent}{format_time(start)} {format_time(elapsed)} {indent2}{event}") 198 | -------------------------------------------------------------------------------- /big/metadata.py: -------------------------------------------------------------------------------- 1 | import big 2 | from .version import Version 3 | 4 | version = Version(big.__version__) 5 | 6 | del Version 7 | del big 8 | -------------------------------------------------------------------------------- /big/scheduler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | _license = """ 4 | big 5 | Copyright 2022-2024 Larry Hastings 6 | All rights reserved. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a 9 | copy of this software and associated documentation files (the "Software"), 10 | to deal in the Software without restriction, including without limitation 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 12 | and/or sell copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included 16 | in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | """ 26 | 27 | __all__ = ['Regulator', 'SingleThreadedRegulator', 'ThreadSafeRegulator', 'Scheduler'] 28 | 29 | 30 | from abc import abstractmethod 31 | import threading 32 | from .heap import Heap 33 | import sys 34 | import time 35 | 36 | 37 | 38 | class InertContextManager: 39 | def __enter__(self): 40 | pass 41 | 42 | def __exit__(self, exception_type, exception, traceback): 43 | pass 44 | 45 | inert_context_manager = InertContextManager() 46 | 47 | class Regulator: 48 | """ 49 | An abstract base class for Scheduler regulators. 50 | 51 | A "regulator" handles all the details about time 52 | for a Scheduler. Scheduler objects don't actually 53 | understand time; it's all abstracted away by the 54 | Regulator. 55 | 56 | You can implement your own Regulator and use it 57 | with Scheduler. Your Regulator subclass needs to 58 | implement a minimum of three methods: 'now', 59 | 'sleep', and 'wake'. It must also provide an 60 | attribute called 'lock'. The lock must implement 61 | the context manager protocol, and should lock 62 | the Regulator as needed. 63 | 64 | Normally a Regulator represents time using a 65 | floating-point number, representing a fractional 66 | number of seconds since some epoch. But this 67 | isn't strictly necessary. Any Python object 68 | that fulfills these requirements will work: 69 | 70 | * The time class must implement `__le__`, `__eq__`, `__add__`, 71 | and `__sub__`, and these operations must be consistent in the 72 | same way they are for number objects. 73 | * If `a` and `b` are instances of the time class, 74 | and `a.__le__(b)` is true, then `a` must either be 75 | an earlier time, or a smaller interval of time. 76 | * The time class must also implement rich comparison 77 | with numbers (integers and floats), and `0` must 78 | represent both the earliest time and a zero-length 79 | interval of time. 80 | """ 81 | 82 | # A context manager that provides thread-safety 83 | # as appropriate. The Schedule will never recursively 84 | # enter the lock (it doesn't need to be a threading.RLock). 85 | lock = inert_context_manager 86 | 87 | @abstractmethod 88 | def now(self): # pragma: no cover 89 | """ 90 | Returns the current time in local units. 91 | Must be monotonically increasing; for any 92 | two calls to now during the course of the 93 | program, the later call must *never* 94 | have a lower value than the earlier call. 95 | 96 | A Scheduler will only call this method while 97 | holding this regulator's lock. 98 | """ 99 | pass 100 | 101 | @abstractmethod 102 | def sleep(self, interval): # pragma: no cover 103 | """ 104 | Sleeps for some amount of time, in local units. 105 | Must support an interval of 0, which should 106 | represent not sleeping. (Though it's preferable 107 | that an interval of 0 yields the rest of the 108 | current thread's remaining time slice back to 109 | the operating system.) 110 | 111 | If wake is called on this Regulator object while a 112 | different thread has called this function to sleep, 113 | sleep must abandon the rest of the sleep interval 114 | and return immediately. 115 | 116 | A Scheduler will only call this method while 117 | *not* holding this regulator's lock. 118 | """ 119 | pass 120 | 121 | @abstractmethod 122 | def wake(self): # pragma: no cover 123 | """ 124 | Aborts all current calls to sleep on this 125 | Regulator, across all threads. 126 | 127 | A Scheduler will only call this method while 128 | holding this regulator's lock. 129 | """ 130 | pass 131 | 132 | 133 | class SingleThreadedRegulator(Regulator): 134 | """ 135 | An implementation of Regulator designed for 136 | use in single-threaded programs. It provides 137 | no thread safety, but is much higher performance 138 | than thread-safe Regulator implementations. 139 | """ 140 | 141 | def __repr__(self): # pragma: no cover 142 | return f"" 143 | 144 | def now(self): 145 | return time.monotonic() 146 | 147 | def wake(self): 148 | pass 149 | 150 | def sleep(self, interval): 151 | time.sleep(interval) 152 | 153 | 154 | default_regulator = SingleThreadedRegulator() 155 | 156 | 157 | class ThreadSafeRegulator(Regulator): 158 | """ 159 | A thread-safe Regulator object designed 160 | for use in multithreaded programs. 161 | It uses Python's threading.Event and 162 | threading.Lock. 163 | """ 164 | 165 | def __init__(self): 166 | self.event = threading.Event() 167 | self.lock = threading.Lock() 168 | 169 | def __repr__(self): # pragma: no cover 170 | return f"" 171 | 172 | def now(self): 173 | return time.monotonic() 174 | 175 | def wake(self): 176 | self.event.set() 177 | self.event.clear() 178 | 179 | def sleep(self, interval): 180 | self.event.wait(interval) 181 | 182 | 183 | DEFAULT_PRIORITY = 100 184 | 185 | class Event: 186 | """ 187 | Wraps event objects in a Scheduler's queue. 188 | 189 | Only supports one method: cancel(), which cancels 190 | the event. (If the event isn't currently scheduled, 191 | raises ValueError.) 192 | """ 193 | def __init__(self, scheduler, event, time, priority, sequence): 194 | self.scheduler = scheduler 195 | self.event = event 196 | self.time = time 197 | self.priority = priority 198 | self.sequence = sequence 199 | 200 | def __lt__(self, other): 201 | if self.time < other.time: 202 | return True 203 | if self.time > other.time: 204 | return False 205 | 206 | if self.priority < other.priority: 207 | return True 208 | if self.priority > other.priority: 209 | return False 210 | 211 | if self.sequence < other.sequence: 212 | return True 213 | 214 | return False 215 | 216 | def __repr__(self): # pragma: no cover 217 | return f"" 218 | 219 | def cancel(self): 220 | self.scheduler.cancel(self) 221 | 222 | 223 | class Scheduler: 224 | """ 225 | A replacement for Python's sched.scheduler object, 226 | adding full threading support and a modern Python interface. 227 | 228 | Python's sched.scheduler object is unfixable; its interface 229 | means it cannot correctly handle some scenarios: 230 | * If one thread has called sched.scheduler.run, 231 | and the next scheduled event will occur at time T, 232 | and a second thread schedules a new event which 233 | occurs at a time < T, sched.scheduler.run won't 234 | return any events to the first thread until time T. 235 | * If one thread has called sched.scheduler.run, 236 | and the next scheduled event will occur at time T, 237 | and a second thread cancels all events, 238 | sched.scheduler.run won't exit until time T. 239 | 240 | Also, sched.scheduler is thirty years behind the times in 241 | Python API design. Its events are callbacks, which it 242 | calls. Scheduler fixes this: its events are objects, 243 | and you iterate over the Scheduler object to receive 244 | events as they become due. 245 | 246 | Scheduler also benefits from thirty years of improvements 247 | to sched.scheduler. In particular, big reimplements the 248 | bulk of the sched.scheduler test suite, to ensure that 249 | Scheduler never repeats the historical problems discovered 250 | over the lifetime of sched.scheduler. 251 | 252 | The only argument to Scheduler is an instance of Regulator, 253 | that provides time and thread-safety to the Scheduler. 254 | By default Scheduler uses an instance of 255 | SingleThreadedRegulator, which is not thread-safe. 256 | 257 | (If you need the scheduler to be thread-safe, pass in 258 | an instance of a thread-safe Regulator class like 259 | ThreadSafeRegulator.) 260 | """ 261 | 262 | def __init__(self, regulator=default_regulator): 263 | self.regulator = regulator 264 | self.heap = Heap() 265 | self.event_id_counter = 0 266 | 267 | def schedule(self, o, time, *, absolute=False, priority=DEFAULT_PRIORITY): 268 | """Schedule a new event to be yielded at a specific future time. 269 | 270 | By default, "time" is relative to the current time. 271 | If absolute is true, "time" is an absolute time. 272 | 273 | "priority" represents the importance of the event. Lower numbers 274 | are more important. There are no predefined priorities; you may 275 | assign priorities however you like. 276 | 277 | Returns an object which can be used to remove the event from the queue: 278 | o = scheduler.schedule(my_event, 5) 279 | o.cancel() # cancels the event 280 | Alternately you can pass in this object to the scheduler's cancel method: 281 | o = scheduler.schedule(my_event, 5) 282 | scheduler.cancel(o) # also cancels the event 283 | """ 284 | with self.regulator.lock: 285 | if not absolute: 286 | time += self.regulator.now() 287 | self.event_id_counter += 1 288 | event = Event(self, o, time, priority, self.event_id_counter) 289 | self.heap.append(event) 290 | self.regulator.wake() 291 | return event 292 | 293 | def cancel(self, event): 294 | """ 295 | Cancel a scheduled event. 296 | 297 | event must be a value returned by add(). 298 | If the event is not in the queue, raises ValueError. 299 | """ 300 | with self.regulator.lock: 301 | self.heap.remove(event) 302 | self.regulator.wake() 303 | 304 | def __bool__(self): 305 | """ 306 | Returns True if the scheduler contains any scheduled events. 307 | This includes both events scheduled for the future, 308 | as well as events that have already expired but have 309 | yet to be yielded by the Scheduler. 310 | """ 311 | with self.regulator.lock: 312 | return bool(self.heap) 313 | 314 | @property 315 | def queue(self): 316 | """ 317 | A list of the currently scheduled Event objects, 318 | in the order they will be yielded. 319 | """ 320 | 321 | # Note: In many places I refer to the scheduler's "queue". 322 | # This isn't a "queue" as in Python's built-in "queue" module. 323 | # The Scheduler's "queue" is a conceptual ordered list of 324 | # events in the order they'll be yielded. (It actually uses 325 | # a "heap", as in Python's built-in "heap" module.) The 326 | # Python scheduler uses the term "queue" a lot, including 327 | # having this exact property, so I kept it. 328 | 329 | with self.regulator.lock: 330 | return self.heap.queue 331 | 332 | def _next(self, blocking=True): 333 | # Why a loop? In case we get woken up early. 334 | # That can happen when the queue changes. 335 | # 336 | # Also, I swear that back in my early Win32 days, 337 | # timers weren't perfect, and the Sleep call would 338 | # occasionally wake up slightly early. 339 | 340 | while True: 341 | 342 | with self.regulator.lock: 343 | if not self.heap: 344 | raise StopIteration 345 | 346 | now = self.regulator.now() 347 | ev = self.heap[0] 348 | time_to_next_event = ev.time - now 349 | 350 | if time_to_next_event <= 0: 351 | self.heap.popleft() 352 | return ev.event 353 | 354 | # Don't sleep while holding the lock! 355 | # 356 | # Do this instead: 357 | # * Fetch the sleep method while 358 | # holding the lock, then 359 | # * Release the lock, then 360 | # * Sleep. 361 | sleep = self.regulator.sleep 362 | 363 | if not blocking: 364 | raise StopIteration 365 | 366 | # assert time_to_next_event > 0 367 | sleep(time_to_next_event) 368 | 369 | def __iter__(self): 370 | return self 371 | 372 | def __next__(self): 373 | return self._next() 374 | 375 | 376 | class NonBlockingSchedulerIterator: 377 | def __init__(self, scheduler): 378 | self.s = scheduler 379 | 380 | def __iter__(self): 381 | return self 382 | 383 | def __next__(self): 384 | return self.s._next(blocking=False) 385 | 386 | def non_blocking(self): 387 | """ 388 | A non-blocking iterator. Yields all events 389 | that are currently ready, then stops iteration. 390 | """ 391 | return self.NonBlockingSchedulerIterator(self) 392 | -------------------------------------------------------------------------------- /big/state.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | _license = """ 4 | big 5 | Copyright 2022-2024 Larry Hastings 6 | All rights reserved. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a 9 | copy of this software and associated documentation files (the "Software"), 10 | to deal in the Software without restriction, including without limitation 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 12 | and/or sell copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included 16 | in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | """ 26 | 27 | 28 | from functools import update_wrapper 29 | from inspect import signature 30 | 31 | __all__ = [] 32 | 33 | def export(o): 34 | __all__.append(o.__name__) 35 | return o 36 | 37 | 38 | @export 39 | class State: 40 | """ 41 | Base class for state machine state implementation classes. 42 | Use of this base class is optional; states can be instances 43 | of any type except types.NoneType. 44 | """ 45 | 46 | def on_enter(self): 47 | """ 48 | Called when entering this state. Optional. 49 | """ 50 | pass 51 | 52 | def on_exit(self): 53 | """ 54 | Called when exiting this state. Optional. 55 | """ 56 | pass 57 | 58 | 59 | 60 | @export 61 | class TransitionError(RecursionError): 62 | """ 63 | Exception raised when attempting to execute an illegal state transition. 64 | 65 | There are only two types of illegal state transitions: 66 | 67 | * An attempted state transition while we're in the process 68 | of transitioning to another state. In other words, 69 | if state_manager is your StateManager object, you can't 70 | set state_manager.state when state_manager.next is not None. 71 | 72 | * An attempt to transition to the current state. 73 | This is illegal: 74 | state_manager = StateManager() 75 | state_manager.state = foo 76 | state_manager.state = foo # <-- this statement raises TransitionError 77 | 78 | (Note that transitioning to a different but *identical* object 79 | is permitted.) 80 | """ 81 | pass 82 | 83 | 84 | 85 | @export 86 | class StateManager: 87 | """ 88 | Simple, Pythonic state machine manager. 89 | 90 | Has three public attributes: 91 | 92 | state is the current state. You transition from 93 | one state to another by assigning to this attribute. 94 | 95 | next is the state the StateManager is transitioning to, 96 | if it's currently in the process of transitioning to a 97 | new state. If the StateManager object isn't currently 98 | transitioning to a new state, its next attribute is None. 99 | While the manager is currently transitioning to a new 100 | state, it's illegal to start a second transition. (In 101 | other words: you can't assign to state while next is not 102 | None.) 103 | 104 | observers is a list of callables that get called 105 | during any state transition. It's initially empty. 106 | * The callables should accept one positional argument, 107 | which is the state manager. 108 | * Since observers are called during the state transition, 109 | they aren't permitted to initiate state transitions. 110 | * You're permitted to modify the list of observers 111 | at any time. If you modify the list of observers 112 | from an observer call, StateManager will still use 113 | the old list for the remainder of that transition. 114 | 115 | The constructor takes the following parameters: 116 | 117 | state is the initial state. It can be any valid 118 | state object; by default, any Python value can be a state 119 | except None. (But also see the state_class parameter below.) 120 | 121 | on_enter represents a method call on states called when 122 | entering that state. The value itself is a string used 123 | to look up an attribute on state objects; by default 124 | on_enter is the string 'on_enter', but it can be any legal 125 | Python identifier string or any false value. 126 | If on_enter is a valid identifier string, and this StateMachine 127 | object transitions to a state object O, and O has an attribute 128 | with this name, StateMachine will call that attribute (with no 129 | arguments) immediately after transitioning to that state. 130 | Passing in a false value for on_enter disables this behavior. 131 | on_enter is called immediately after the transition is complete, 132 | which means you're expressly permitted to make a state transition 133 | inside an on_enter call. If defined, on_exit will be called on 134 | the initial state object, from inside the StateManager constructor. 135 | 136 | on_exit is similar to on_enter, except the attribute is 137 | called when transitioning *away* from a state object. 138 | Its default value is 'on_exit'. on_exit is called 139 | *during* the state transition, which means you're expressly 140 | forbidden from making a state transition inside an on_exit call. 141 | 142 | state_class is either None or a class. If it's a class, 143 | the StateManager object will require every value assigned 144 | to its 'state' attribute to be an instance of that class. 145 | 146 | To transition to a new state, assign to the 'state' attribute. 147 | 148 | If state_class is None, you may use *any* value as a state 149 | except None. 150 | 151 | It's illegal to assign to 'state' while currently 152 | transitioning to a new state. (Or, in other words, 153 | at any time self.next is not None.) 154 | 155 | It's illegal to attempt to transition to the current 156 | state. (If state_manager.state is already foo, 157 | setting "state_manager.state = foo" raises an exception.) 158 | 159 | If the current state object has an 'on_exit' attribute, 160 | it will be called as a function with zero arguments during 161 | the the transition to the next state. 162 | 163 | If you assign an object to 'state' that has an 'on_enter' 164 | attribute, it will be called as a function with zero 165 | arguments immediately after we have transitioned to that 166 | state. 167 | 168 | If you have an StateManager instance called "state_manager", 169 | and you transition it to "new_state": 170 | 171 | state_manager.state = new_state 172 | 173 | StateManager will execute the following sequence of actions: 174 | 175 | * Set self.next to 'new_state'. 176 | * At of this moment your StateManager instance is 177 | "transitioning" to the new state. 178 | * If self.state has an attribute called 'on_exit', 179 | call self.state.on_exit(). 180 | * For every object 'o' in the observer list, call o(self). 181 | * Set self.next to None. 182 | * Set self.state to 'new_state'. 183 | * As of this moment, the transition is complete, and 184 | your StateManager instance is now "in" the new state. 185 | * If self.state has an attribute called 'on_enter', 186 | call self.state.on_enter(). 187 | 188 | You may also be interested in the `accessor` and `dispatch` 189 | decorators in this module. 190 | """ 191 | 192 | __state = None 193 | @property 194 | def state(self): 195 | return self.__state 196 | 197 | @state.setter 198 | def state(self, state): 199 | if (self.state_class is not None) and (not isinstance(state, self.state_class)): 200 | raise TypeError(f"invalid state object {state}, must be an instance of {self.state_class.__name__}") 201 | if state is None: 202 | raise ValueError("state can't be None") 203 | if self.__next is not None: 204 | raise TransitionError(f"can't start new state transition to {state}, still processing transition to {self.__next}") 205 | 206 | # 207 | # Use of the "is" operator is deliberate here. 208 | # 209 | # StateManager doesn't permit transitioning to the same 210 | # exact state *object.* But you are explicitly permitted 211 | # to transition to an *identical* object... as long as it's 212 | # a different object. 213 | # 214 | # Why does StateManager prevent you from transitioning 215 | # to the same state? 216 | # 217 | # a) Conceptually it's nonsense. You can't "transition" 218 | # to the state you're currently in. That's akin to saying 219 | # "Please travel to exactly where you are". 220 | # b) Attempting to do this is almost certainly due to a bug. 221 | # And you want Python to raise an exception when you have 222 | # a bug... don't you? 223 | # 224 | if state is self.__state: 225 | raise TransitionError(f"can't transition to {state}, it's already the current state") 226 | 227 | self.__next = state 228 | # as of this moment we are "transitioning" to a new state. 229 | 230 | if self.on_exit: 231 | on_exit = getattr(self.__state, self.on_exit, None) 232 | if on_exit is not None: 233 | on_exit() 234 | if self.observers: 235 | if self.observers != self._observers_copy: 236 | self._observers_copy = list(self.observers) 237 | self._observers_tuple = tuple(self.observers) 238 | for o in self._observers_tuple: 239 | o(self) 240 | 241 | self.__state = state 242 | self.__next = None 243 | # as of this moment we are "in" our new state, the transition is over. 244 | # (it's explicitly permitted to start a new state transition from inside enter().) 245 | 246 | if self.on_enter: 247 | on_enter = getattr(self.__state, self.on_enter, None) 248 | if on_enter is not None: 249 | on_enter() 250 | 251 | __next = None 252 | @property 253 | def next(self): 254 | return self.__next 255 | 256 | def __init__(self, 257 | state, 258 | *, 259 | on_enter='on_enter', 260 | on_exit='on_exit', 261 | state_class=None, 262 | ): 263 | if not ((state_class is None) or isinstance(state_class, type)): 264 | raise TypeError(f"state_class value {state_class} is invalid, it must either be None or a class") 265 | self.state_class = state_class 266 | 267 | if not on_enter: 268 | on_enter = None 269 | elif not (isinstance(on_enter, str) and on_enter.isidentifier()): 270 | raise ValueError(f'on_enter must be a string containing a valid Python identifier, not {on_enter}') 271 | self.on_enter = on_enter 272 | 273 | if not on_exit: 274 | on_exit = None 275 | elif not (isinstance(on_exit, str) and on_exit.isidentifier()): 276 | raise ValueError(f'on_exit must be a string containing a valid Python identifier, not {on_exit}') 277 | self.on_exit = on_exit 278 | 279 | self.observers = [] 280 | self._observers_copy = [] 281 | self._observers_tuple = () 282 | self.state = state 283 | 284 | def __repr__(self): 285 | return f"<{type(self).__name__} state={self.state} next={self.next} observers={self.observers}>" 286 | 287 | 288 | @export 289 | def accessor(attribute='state', state_manager='state_manager'): 290 | """ 291 | Class decorator. Adds a convenient state accessor attribute to your class. 292 | 293 | Example: 294 | 295 | @accessor() 296 | class StateMachine: 297 | def __init__(self): 298 | self.state_manager = StateManager(self.InitialState) 299 | sm = StateMachine() 300 | 301 | This creates an attribute of StateMachine called "state". 302 | 'state' behaves identically to the "state" attribute 303 | of the "state_manager" attribute of StateMachine. 304 | 305 | It evaluates to the same value: 306 | 307 | sm.state == sm.state_manager.state 308 | 309 | And setting it sets the state on the StateManager object. 310 | These two statements now do the same thing: 311 | 312 | sm.state_manager.state = new_state 313 | sm.state = new_state 314 | 315 | By default, this decorator assumes your StateManager instance 316 | is stored in 'state_manager', and you want to name the new 317 | accessor attribute 'state'. You can override these defaults; 318 | the first parameter, 'attribute', is the name that will be used 319 | for the new accessor attribute, and the second parameter, 'state_manager', 320 | should be the name of the attribute where your StateManager instance 321 | is stored. 322 | """ 323 | def accessor(cls): 324 | def getter(self): 325 | i = getattr(self, state_manager) 326 | return getattr(i, 'state') 327 | def setter(self, value): 328 | i = getattr(self, state_manager) 329 | setattr(i, 'state', value) 330 | setattr(cls, attribute, property(getter, setter)) 331 | return cls 332 | return accessor 333 | 334 | 335 | @export 336 | def dispatch(state_manager='state_manager', *, prefix='', suffix=''): 337 | """ 338 | Decorator for state machine event methods, 339 | dispatching the event from the state machine object 340 | to its current state. 341 | 342 | dispatch helps with the following scenario: 343 | * You have your own state machine class which contains 344 | a StateManager object. 345 | * You want your state machine class to have methods 346 | representing events. 347 | * Rather than handle those events in your state machine 348 | object itself, you want to dispatch them to the current 349 | state. 350 | 351 | For example, instead of writing this: 352 | 353 | class StateMachine: 354 | def __init__(self): 355 | self.state_manager = StateManager(self.InitialState) 356 | 357 | def on_sunrise(self, time, *, verbose=False): 358 | return self.state_manager.state.on_sunrise(time, verbose=verbose) 359 | 360 | you can literally write this, which does the same thing: 361 | 362 | class StateMachine: 363 | def __init__(self): 364 | self.state_manager = StateManager(self.InitialState) 365 | 366 | @dispatch() 367 | def on_sunrise(self, time, *, verbose=False): 368 | ... 369 | 370 | Here, the on_sunrise function you wrote is actually thrown away. 371 | (That's why the body is simply one "..." statement.) Your function 372 | is replaced with a function that gets the "state_manager" attribute 373 | from self, then gets the 'state' attribute from that StateManager 374 | instance, then calls a method with the same name as the decorated 375 | function, passing in using *args and **kwargs. 376 | 377 | Note that, as a stylistic convention, you're encouraged to literally 378 | use a single ellipsis as the body of these functions, like so: 379 | 380 | class StateMachine: 381 | @dispatch() 382 | def on_sunrise(self, time, *, verbose=False): 383 | ... 384 | 385 | This is a visual cue to readers that the body of the function 386 | doesn't matter. 387 | 388 | The state_manager argument to the decorator should be the name of 389 | the attribute where the StateManager instance is stored in self. 390 | The default is 'state_manager'. For example, if you store your 391 | state manager in the attribute 'smedley', you'd decorate with: 392 | 393 | @dispatch('smedley') 394 | 395 | The prefix and suffix arguments are strings added to the 396 | beginning and end of the method call we call on the current state. 397 | For example, if you want the method you call to have an active verb 398 | form (e.g. 'reset'), but you want it to directly call an event 399 | handler that starts with 'on_' by convention (e.g. 'on_reset'), 400 | you could do this: 401 | 402 | @dispatch(prefix='on_') 403 | def reset(self): 404 | ... 405 | 406 | This is equivalent to: 407 | 408 | def reset(self): 409 | return self.state_manager.state.on_reset() 410 | 411 | Note that you can cache the return value from calling 412 | dispatch and use it multiple times, like so: 413 | 414 | my_dispatch = dispatch('smedley', prefix='on_') 415 | 416 | @my_dispatch 417 | def reset(self): 418 | ... 419 | 420 | @my_dispatch 421 | def sunrise(self): 422 | ... 423 | 424 | """ 425 | def dispatch(fn): 426 | def wrapper(self, *args, **kwargs): 427 | i = getattr(self, state_manager) 428 | method = getattr(i.state, f'{prefix}{fn.__name__}{suffix}') 429 | return method(*args, **kwargs) 430 | update_wrapper(wrapper, fn) 431 | wrapper.__signature__ = signature(fn) 432 | return wrapper 433 | return dispatch 434 | -------------------------------------------------------------------------------- /big/time.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | _license = """ 4 | big 5 | Copyright 2022-2024 Larry Hastings 6 | All rights reserved. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a 9 | copy of this software and associated documentation files (the "Software"), 10 | to deal in the Software without restriction, including without limitation 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 12 | and/or sell copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included 16 | in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | """ 26 | 27 | import calendar 28 | from datetime import date, datetime, timezone 29 | try: # pragma: no cover 30 | import dateutil.parser 31 | have_dateutils = True 32 | except ImportError: # pragma: no cover 33 | have_dateutils = False 34 | import time 35 | 36 | # I'm on my way, I'm making it 37 | 38 | __all__ = [] 39 | 40 | def export(o): 41 | __all__.append(o.__name__) 42 | return o 43 | 44 | 45 | 46 | _timestamp_human_format = "%Y/%m/%d %H:%M:%S" 47 | 48 | @export 49 | def timestamp_human(t=None, want_microseconds=None): 50 | """ 51 | Return a timestamp string formatted in a pleasing way 52 | using the currently-set local timezone. This format 53 | is intended for human readability; for computer-parsable 54 | time, use timestamp_3339Z(). 55 | 56 | Example timestamp: 57 | '2021/05/24 23:42:49.099437' 58 | 59 | t can be one of several types: 60 | If t is None, timestamp_human uses the current local time. 61 | 62 | If t is an int or float, it's interpreted as seconds 63 | since the epoch. 64 | 65 | If t is a time.struct_time or datetime.datetime object, 66 | it's converted to the local timezone. 67 | 68 | If want_microseconds is true, the timestamp will end with 69 | the microseconds, represented as ".######". If want_microseconds 70 | is false, the timestamp will not include the microseconds. 71 | If want_microseconds is None (the default), the timestamp 72 | ends with microseconds if t is a type that can represent 73 | fractional seconds: a float, a datetime object, or the 74 | value None. 75 | """ 76 | if isinstance(t, time.struct_time): 77 | if t.tm_zone == "GMT": 78 | t = calendar.timegm(t) 79 | else: 80 | t = time.mktime(t) 81 | # force None to False 82 | want_microseconds = bool(want_microseconds) 83 | 84 | if t is None: 85 | t = datetime.now() 86 | elif isinstance(t, (int, float)): 87 | if want_microseconds == None: 88 | want_microseconds = isinstance(t, float) 89 | t = datetime.fromtimestamp(t) 90 | elif isinstance(t, datetime): 91 | if t.tzinfo != None: 92 | t = t.astimezone(None) 93 | else: 94 | raise TypeError(f"unrecognized type {type(t)}") 95 | 96 | if want_microseconds == None: 97 | # if it's still None, t must be a type that supports microseconds 98 | want_microseconds = True 99 | 100 | s = t.strftime(_timestamp_human_format) 101 | if want_microseconds: 102 | s = f"{s}.{t.microsecond:06d}" 103 | return s 104 | 105 | 106 | _timestamp_3339Z_format_base = "%Y-%m-%dT%H:%M:%S" 107 | _timestamp_3339Z_format = f"{_timestamp_3339Z_format_base}Z" 108 | _timestamp_3339Z_microseconds_format = f"{_timestamp_3339Z_format_base}.%fZ" 109 | 110 | @export 111 | def timestamp_3339Z(t=None, want_microseconds=None): 112 | """ 113 | Return a timestamp string in RFC 3339 format, in the UTC 114 | time zone. This format is intended for computer-parsable 115 | timestamps; for human-readable timestamps, use timestamp_human(). 116 | 117 | Example timestamp: 118 | '2021-05-25T06:46:35.425327Z' 119 | 120 | t may be one of several types: 121 | If t is None, timestamp_3339Z uses the current time in UTC. 122 | 123 | If t is an int or a float, it's interpreted as seconds 124 | since the epoch in the UTC time zone. 125 | 126 | If t is a time.struct_time object or datetime.datetime 127 | object, and it's not in UTC, it's converted to UTC. 128 | (Technically, time.struct_time objects are converted to GMT, 129 | using time.gmtime. Sorry, pedants!) 130 | 131 | If want_microseconds is true, the timestamp ends with 132 | microseconds, represented as a period and six digits between 133 | the seconds and the 'Z'. If want_microseconds 134 | is false, the timestamp will not include this text. 135 | If want_microseconds is None (the default), the timestamp 136 | ends with microseconds if t is a type that can represent 137 | fractional seconds: a float, a datetime object, or the 138 | value None. 139 | """ 140 | if isinstance(t, time.struct_time): 141 | if t.tm_zone == "GMT": 142 | t = calendar.timegm(t) 143 | else: 144 | t = time.mktime(t) 145 | want_microseconds = bool(want_microseconds) 146 | 147 | if t is None: 148 | t = datetime.now(timezone.utc) 149 | elif isinstance(t, (int, float)): 150 | t = datetime.fromtimestamp(t, timezone.utc) 151 | if want_microseconds == None: 152 | want_microseconds = isinstance(t, float) 153 | elif isinstance(t, datetime): 154 | if t.tzinfo != timezone.utc: 155 | t = t.astimezone(timezone.utc) 156 | else: 157 | raise TypeError(f"unrecognized type {type(t)}") 158 | 159 | if want_microseconds == None: 160 | # if want_microseconds is *still* None, 161 | # t must be a type that supports microseconds 162 | want_microseconds = True 163 | 164 | if want_microseconds: 165 | f = _timestamp_3339Z_microseconds_format 166 | else: 167 | f = _timestamp_3339Z_format 168 | 169 | return t.strftime(f) 170 | 171 | 172 | @export 173 | def datetime_set_timezone(d, timezone): 174 | """ 175 | Returns a new datetime.datetime object identical 176 | to d but with its tzinfo set to timezone. 177 | """ 178 | return datetime( 179 | year = d.year, 180 | month = d.month, 181 | day = d.day, 182 | hour = d.hour, 183 | minute = d.minute, 184 | second = d.second, 185 | microsecond = d.microsecond, 186 | tzinfo = timezone, 187 | fold = d.fold, 188 | ) 189 | 190 | @export 191 | def datetime_ensure_timezone(d, timezone): 192 | """ 193 | Ensures that a datetime.datetime object has 194 | a timezone set. 195 | 196 | If d has a timezone set, returns d. 197 | Otherwise, returns a new datetime.datetime 198 | object equivalent to d with its tzinfo set 199 | to timezone. 200 | """ 201 | if d.tzinfo: 202 | return d 203 | return datetime_set_timezone(d, timezone) 204 | 205 | if have_dateutils: 206 | @export 207 | def parse_timestamp_3339Z(s, *, timezone=None): 208 | """ 209 | Parses a timestamp string returned by timestamp_3339Z. 210 | Returns a datetime.datetime object. 211 | 212 | timezone is an optional default timezone, and should 213 | be a datetime.tzinfo object (or None). If provided, 214 | and the time represented in the string doesn't specify 215 | a timezone, the 'tzinfo' attribute of the returned object 216 | will be explicitly set to timezone. 217 | """ 218 | d = dateutil.parser.parse(s) 219 | assert d 220 | d = datetime_ensure_timezone(d, timezone) 221 | return d 222 | -------------------------------------------------------------------------------- /big/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | The big package is a grab-bag of cool code for use in your programs. 5 | 6 | Think big! 7 | """ 8 | 9 | _license = """ 10 | big 11 | Copyright 2022-2024 Larry Hastings 12 | All rights reserved. 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a 15 | copy of this software and associated documentation files (the "Software"), 16 | to deal in the Software without restriction, including without limitation 17 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 18 | and/or sell copies of the Software, and to permit persons to whom the 19 | Software is furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included 22 | in all copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 25 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 26 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 27 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 28 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 29 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 30 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | """ 32 | 33 | # 34 | # This is based on reading PEP 440: 35 | # https://peps.python.org/pep-0440/ 36 | # and the updated version at PyPI, the "version-specifiers" doc: 37 | # https://packaging.python.org/en/latest/specifications/version-specifiers/ 38 | 39 | 40 | import functools 41 | import itertools 42 | import math 43 | import re 44 | import sys 45 | 46 | try: # pragma: nocover 47 | from packaging.version import Version as _packagingVersion 48 | except ImportError: # pragma: nocover 49 | _packagingVersion = None 50 | 51 | 52 | _release_level_allowed_values = ('alpha', 'beta', 'rc', 'final') 53 | _release_level_normalize = {'a': 'alpha', 'b': 'beta', 'c': 'rc', 'pre': 'rc', 'preview': 'rc'} 54 | _release_level_to_integer = {'alpha': -3, 'beta': -2, 'rc': -1, 'final': 0} 55 | _release_level_to_str = {'alpha': 'a', 'beta': 'b', 'rc': 'rc', 'final': ''} 56 | 57 | _sys_version_info_type = type(sys.version_info) 58 | # _sys_version_info_release_level_normalize = { 'alpha': 'a', 'beta': 'b', 'candidate': 'rc', 'final': ''} 59 | _sys_version_info_release_level_normalize = { 'candidate': 'rc', 'final': ''} 60 | 61 | # 62 | # regular expression is straight out of PEP 440. 63 | # 64 | 65 | VERSION_PATTERN = r"^\s*" + r""" 66 | v? 67 | (?: 68 | (?:(?P[0-9]+)!)? # epoch 69 | (?P[0-9]+(?:\.[0-9]+)*) # release segment 70 | (?P
                                          # pre-release
 71 |             [-_\.]?
 72 |             (?P(a|b|c|rc|alpha|beta|pre|preview))
 73 |             [-_\.]?
 74 |             (?P[0-9]+)?
 75 |         )?
 76 |         (?P                                         # post release
 77 |             (?:-(?P[0-9]+))
 78 |             |
 79 |             (?:
 80 |                 [-_\.]?
 81 |                 (?Ppost|rev|r)
 82 |                 [-_\.]?
 83 |                 (?P[0-9]+)?
 84 |             )
 85 |         )?
 86 |         (?P                                          # dev release
 87 |             [-_\.]?
 88 |             (?Pdev)
 89 |             [-_\.]?
 90 |             (?P[0-9]+)?
 91 |         )?
 92 |     )
 93 |     (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
 94 | """ + r"\s*$"
 95 | 
 96 | _re_parse_version = re.compile(VERSION_PATTERN, re.VERBOSE | re.IGNORECASE).match
 97 | 
 98 | del VERSION_PATTERN
 99 | 
100 | _re_is_valid_local_segment = re.compile("^[A-Za-z0-9]+$").match
101 | 
102 | 
103 | @functools.total_ordering
104 | class Version:
105 |     def __init__(self, s=None, *, epoch=None, release=None, release_level=None, serial=None, post=None, dev=None, local=None):
106 |         """
107 |         Constructs a `Version` object, which represents a version number.
108 | 
109 |         You may define the version one of two ways:
110 | 
111 |         * by passing in a string to the `s` positional parameter specifying the version.
112 |           Example: `Version("1.3.5rc3")`
113 |         * by passing in keyword-only arguments setting the specific fields of the version.
114 |           Example: `Version(release=(1, 3, 5), release_level="rc", serial=3)`
115 | 
116 |         This object conforms to the PEP 440 version scheme,
117 |         parsing version strings using the PEP's official regular
118 |         expression.
119 | 
120 |         For more detailed information, see big's README file.
121 |         """
122 |         if s is None:
123 |             if release is None:
124 |                 raise ValueError("you must specify either a version string or explicit keyword arguments")
125 | 
126 |             if epoch is None:
127 |                 epoch = 0
128 |             elif not (isinstance(epoch, int) and (epoch >= 0)):
129 |                 raise ValueError(f"epoch must be non-negative int or None, not {epoch!r}")
130 | 
131 |             if not (isinstance(release, tuple) and release and all(isinstance(element, int) for element in release)):
132 |                 raise ValueError(f"release must be a tuple of length 1+ containing only ints, not {release!r}")
133 | 
134 |             original_release_level = release_level
135 |             if release_level is None:
136 |                 release_level = 'final'
137 |             if not (release_level in _release_level_allowed_values):
138 |                 raise ValueError(f"release_level {release_level!r} must be one of 'alpha', 'beta', 'rc', 'final', or None")
139 | 
140 |             if serial is None:
141 |                 serial = 0
142 |             elif not (isinstance(serial, int) and (serial >= 0)):
143 |                 raise ValueError(f"serial must be non-negative int or None, not {serial!r}")
144 | 
145 |             if not ((post is None) or (isinstance(post, int) and (post >= 0))):
146 |                 raise ValueError(f"post must be non-negative int or None, not {post!r}")
147 | 
148 |             if not ((dev is None) or (isinstance(dev, int) and (dev >= 0))):
149 |                 raise ValueError(f"dev must be non-negative int or None, not {dev!r}")
150 | 
151 |             if not ((local is None) or (isinstance(local, tuple) and local and all(isinstance(element, str) and element and _re_is_valid_local_segment(element) for element in local))):
152 |                 raise ValueError(f"local must be None or a tuple of length 1+ containing only non-empty strings of letters and digits, not {local!r}")
153 | 
154 |         else:
155 |             if (
156 |                    (epoch != None)
157 |                 or (release != None)
158 |                 or (release_level != None)
159 |                 or (serial != None)
160 |                 or (post != None)
161 |                 or (dev != None)
162 |                 or (local != None)
163 |                 ):
164 |                 raise ValueError('you cannot specify both a version string and keyword arguments')
165 | 
166 |             if isinstance(s, _sys_version_info_type):
167 |                 # support the Python sys.version_info object!
168 |                 s2 = f"{s.major}.{s.minor}.{s.micro}"
169 |                 rl = _sys_version_info_release_level_normalize.get(s.releaselevel, s.releaselevel)
170 |                 if rl: # pragma: nocover
171 |                     s2 += rl
172 |                     if s.serial:
173 |                         s2 += f"{s.serial}"
174 |                 s = s2
175 |             elif _packagingVersion and isinstance(s, _packagingVersion): # pragma: nocover
176 |                 # support the packaging.version.Version object.  embrace and extend!
177 |                 s = str(s)
178 |             elif not isinstance(s, str):
179 |                 raise ValueError("you must specify either a version string or explicit keyword arguments")
180 | 
181 |             match = _re_parse_version(s)
182 |             if not match:
183 |                 raise ValueError(f"invalid version initializer {s!r}") from None
184 | 
185 |             # groups furnished by the version-parsing regular expression
186 |             # "epoch"
187 |             # "release", release segment
188 |             # "pre", pre-release
189 |             #    also split up into "pre_l" (pre label, must be 'a'|'b'|'c'|'rc'|'alpha'|'beta'|'pre'|'preview', a==alpha, b==beta, c/pre/review==rc) and "pre_n" (pre number)
190 |             # "post", post release
191 |             #    also split up into "post_n1" (post first number), "post_l" (post label, must be 'post'|'rev'|'r' which are all equivalent), and "post_n2" (post second number)
192 |             #    either "post_n1" is set, or "post_l" and "post_n2" are set.
193 |             # "dev", dev release
194 |             #    also split up into "dev_l" (must be 'dev') and "dev_n" (dev number)
195 |             # "local", local version
196 | 
197 |             d = match.groupdict()
198 |             get = d.get
199 | 
200 |             # if an optional value is not None, and we expect it to be an int,
201 |             # the regular expression guarantees that it's a parsable int,
202 |             # so we don't need to guard against ValueError etc.
203 | 
204 |             # if they didn't supply a value, e.g. there's no epoch in the string,
205 |             # I go ahead and store a None for the attribute.
206 | 
207 |             epoch = get('epoch')
208 |             epoch = int(epoch) if epoch is not None else None
209 | 
210 |             release = [int(_) for _ in get('release').split('.')]
211 |             # if S is a release string, and T is S+".0",
212 |             # S and T represent the same version.
213 |             # so, let's clip off all trailing zero elements on release.
214 |             while (len(release) > 1) and (not release[-1]):
215 |                 release.pop()
216 |             release = tuple(release)
217 | 
218 |             release_level = get('pre_l')
219 |             if release_level is None:
220 |                 release_level = 'final'
221 |             release_level = _release_level_normalize.get(release_level, release_level)
222 |             # don't need to validate this, it's constrained by the regex
223 |             # if release_level not in _release_level_allowed_values:
224 |             #     raise ValueError(f"release_level string must be 'alpha' or 'a', 'beta' or 'b', 'rc' or 'c' or 'pre' or 'preview', or 'final', or unspecified, not {release_level!r}")
225 |             assert release_level in _release_level_allowed_values
226 | 
227 |             serial = get('pre_n')
228 |             if (release_level == 'final'):
229 |                 assert serial is None
230 |             serial = int(serial) if serial is not None else None
231 | 
232 |             post_l = get('post_l')
233 |             post_n1 = get('post_n1')
234 |             post_n2 = get('post_n2')
235 |             if post_n1 is not None:
236 |                 assert (post_l is None) and (post_n2 is None)
237 |                 post = post_n1
238 |             elif post_n2 is not None:
239 |                 assert post_l is not None
240 |                 post = post_n2
241 |             else:
242 |                 post = None
243 | 
244 |             post = int(post) if post is not None else None
245 | 
246 |             dev_l = get('dev_l')
247 |             dev = get('dev_n')
248 |             if dev is not None:
249 |                 assert dev_l is not None
250 |             dev = int(dev) if dev is not None else None
251 | 
252 |             local = original_local = get('local')
253 |             if local is not None:
254 |                 local = tuple(local.replace('-', '.').replace('_', '.').split('.'))
255 | 
256 |         if local:
257 |             # if local is true, it's an iterable of strings.
258 |             # convert elements to ints where possible, leave as strings otherwise.
259 |             compare_local = tuple((2, int(element)) if element.isdigit() else (1, element) for element in local)
260 |         else:
261 |             local = None
262 |             compare_local = ()
263 | 
264 |         self._epoch = epoch
265 |         self._release = release
266 |         self._str_release = ".".join(str(element) for element in release)
267 |         self._release_level = release_level
268 |         self._serial = serial
269 |         self._post = post
270 |         self._dev = dev
271 |         self._local = local
272 | 
273 |         # when ordering:
274 |         #     epoch
275 |         #     release
276 |         #     alpha < beta < rc < final
277 |         #     post, if you have no post you are < a release that has a post
278 |         #     dev, if you have no dev you are > a release that has a dev
279 |         #     local, which is... complicated
280 |         #
281 |         # The version-specifiers document says basically everywhere
282 |         # (epoch, serial, post, dev) that if they don't specify a number,
283 |         # the number is implicitly 0.  see these sections:
284 |         #     "Version epochs"
285 |         #     "Implicit pre-release number" (what I'm calling "serial")
286 |         #     "Implicit development release number"
287 |         #     "Implicit post release number"
288 |         #
289 |         # I infer that to mean that "1.0.dev" and "1.0.dev0" are the same version.
290 |         # But!  "1.0" is *definitely* later than "1.0.dev0".
291 |         #
292 |         # Here's how I handle that: if they didn't specify "field",
293 |         # I'll have a None for that attribute.  I substitute the following value
294 |         # in the comparison tuple.  Ready?
295 |         #
296 |         #       field  |  tuple value
297 |         #       -------+-------
298 |         #       epoch  |  0
299 |         #       serial |  0
300 |         #       dev    |  math.inf
301 |         #       post   | -1
302 |         #
303 |         # What's going on with "dev"?  Any "dev" version is earlier than any
304 |         # non-"dev" version, but ".dev33" < ".dev34".
305 |         #
306 |         # And, with "post", any release with a "post" is later than any
307 |         # release with no "post".
308 | 
309 |         if epoch is None:
310 |             epoch = 0
311 |         release_level = _release_level_to_integer[release_level]
312 |         if serial is None:
313 |             serial = 0
314 |         if dev is None:
315 |             dev = math.inf
316 |         if post is None:
317 |             post = -1
318 |         self._tuple = (epoch, release, release_level, serial, post, dev, compare_local)
319 |         self._format_map = None
320 | 
321 |     def __repr__(self):
322 |         return f"Version({str(self)!r})"
323 | 
324 |     def __str__(self):
325 |         text = []
326 |         append = text.append
327 | 
328 |         if self._epoch:
329 |             append(f"{self._epoch}!")
330 |         append(self._str_release)
331 | 
332 |         release_level = _release_level_to_str.get(self._release_level)
333 |         if not release_level:
334 |             assert not self._serial
335 |         else:
336 |             append(release_level)
337 |             if self._serial:
338 |                 append(str(self._serial))
339 | 
340 |         if self._post is not None:
341 |             append(f".post{self._post}")
342 | 
343 |         if self._dev is not None:
344 |             append(f".dev{self._dev}")
345 | 
346 |         if self._local:
347 |             append("+")
348 |             append(".".join(self._local))
349 | 
350 |         return "".join(text)
351 | 
352 |     def format(self, s):
353 |         """
354 |         Returns a formatted version of s,
355 |         substituting attributes from self
356 |         into s using str.format_map.
357 | 
358 |         For example,
359 | 
360 |            Version("1.3.5").format('{major}.{minor}')
361 | 
362 |         returns the string '1.3'.
363 |         """
364 |         if self._format_map is None:
365 |             self._format_map = {
366 |                 "epoch": self._epoch,
367 |                 "release": self._str_release,
368 |                 "major": self.major,
369 |                 "minor": self.minor,
370 |                 "micro": self.micro,
371 |                 "release_level": self._release_level,
372 |                 "serial": self._serial,
373 |                 "post": self._post,
374 |                 "dev": self._dev,
375 |                 "local": self._local,
376 |             }
377 |         return s.format_map(self._format_map)
378 | 
379 |     def __eq__(self, other):
380 |         if isinstance(other, _sys_version_info_type):
381 |             other = self.__class__(other)
382 |         elif _packagingVersion and isinstance(other, _packagingVersion): # pragma: no cover
383 |             other = self.__class__(str(other))
384 |         elif not isinstance(other, Version):
385 |             return False
386 |         return self._tuple == other._tuple
387 | 
388 |     def __lt__(self, other):
389 |         if isinstance(other, _sys_version_info_type):
390 |             other = self.__class__(other)
391 |         elif _packagingVersion and isinstance(other, _packagingVersion): # pragma: no cover
392 |             other = self.__class__(str(other))
393 |         if not isinstance(other, Version):
394 |             raise TypeError("'<' not supported between instances of 'Version' and '{type(other)}'")
395 |         return self._tuple < other._tuple
396 | 
397 | 
398 |     def __hash__(self):
399 |         return (
400 |             hash(self._epoch)
401 |             ^ hash(self._release)
402 |             ^ hash(self._release_level)
403 |             ^ hash(self._serial)
404 |             ^ hash(self._post)
405 |             ^ hash(self._dev)
406 |             ^ hash(self._local)
407 |             )
408 | 
409 |     @property
410 |     def epoch(self):
411 |         return self._epoch
412 | 
413 |     @property
414 |     def release(self):
415 |         return self._release
416 | 
417 |         self._release_level = release_level
418 |         self._serial = serial
419 |         self._post = post
420 |         self._dev = dev or 0
421 |         self._local = local
422 | 
423 |     @property
424 |     def major(self):
425 |         return self._release[0]
426 | 
427 |     @property
428 |     def minor(self):
429 |         if len(self._release) <= 1:
430 |             return 0
431 |         return self._release[1]
432 | 
433 |     @property
434 |     def micro(self):
435 |         if len(self._release) <= 2:
436 |             return 0
437 |         return self._release[2]
438 | 
439 |     @property
440 |     def release_level(self):
441 |         return self._release_level
442 | 
443 |     # alias for python's stinky non-PEP8 spelling
444 |     @property
445 |     def releaselevel(self):
446 |         return self._release_level
447 | 
448 |     @property
449 |     def serial(self):
450 |         return self._serial
451 | 
452 |     @property
453 |     def post(self):
454 |         return self._post
455 | 
456 |     @property
457 |     def dev(self):
458 |         return self._dev
459 | 
460 |     @property
461 |     def local(self):
462 |         return self._local
463 | 
464 | __all__ = ['Version']
465 | 


--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
 1 | [build-system]
 2 | requires = ["flit"]
 3 | build-backend = "flit.buildapi"
 4 | 
 5 | [project]
 6 | name = "big"
 7 | authors = [
 8 |     {name="Larry Hastings", email="larry@hastings.org"},
 9 |     ]
10 | readme = "README.md"
11 | requires-python = ">=3.6"
12 | 
13 | classifiers = [
14 |     "Intended Audience :: Developers",
15 |     "License :: OSI Approved :: MIT License",
16 |     "Programming Language :: Python :: 3",
17 |     "Programming Language :: Python :: 3.6",
18 |     "Programming Language :: Python :: 3.7",
19 |     "Programming Language :: Python :: 3.8",
20 |     "Programming Language :: Python :: 3.9",
21 |     "Programming Language :: Python :: 3.10",
22 |     "Programming Language :: Python :: 3.11",
23 |     "Programming Language :: Python :: 3.12",
24 |     "Programming Language :: Python :: 3.13",
25 |     ]
26 | dynamic = [
27 |     "description",
28 |     "version",
29 |     ]
30 | 
31 | [project.urls]
32 | Source = "https://github.com/larryhastings/big/"
33 | 
34 | [project.optional-dependencies]
35 | packaging = [
36 |     "packaging",
37 |     ]
38 | time = [
39 |     "python-dateutil",
40 |     ]
41 | test = [
42 |     "python-dateutil",
43 |     "packaging",
44 |     "regex",
45 |     "inflect",
46 |     ]
47 | 


--------------------------------------------------------------------------------
/resources/experiments/time_multisplit.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | 
  3 | _license = """
  4 | big
  5 | Copyright 2022-2023 Larry Hastings
  6 | All rights reserved.
  7 | 
  8 | Permission is hereby granted, free of charge, to any person obtaining a
  9 | copy of this software and associated documentation files (the "Software"),
 10 | to deal in the Software without restriction, including without limitation
 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
 12 | and/or sell copies of the Software, and to permit persons to whom the
 13 | Software is furnished to do so, subject to the following conditions:
 14 | 
 15 | The above copyright notice and this permission notice shall be included
 16 | in all copies or substantial portions of the Software.
 17 | 
 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 25 | """
 26 | 
 27 | 
 28 | import itertools
 29 | from itertools import zip_longest
 30 | import re
 31 | 
 32 | 
 33 | #
 34 | # Two different implementations of multisplit.
 35 | # Both produce the same results (though maybe would need work
 36 | # to pass the latest version of the test suite).
 37 | # multisplit1 is cheap and easy, but wasteful.
 38 | # multisplit2 is a more complicated algorithm but
 39 | #   does a lot less work.
 40 | #
 41 | # result: for trivial inputs, multisplit1 can be slightly
 42 | # faster.  but once the workload rises even slightly,
 43 | # multisplit2 becomes faster.  and the more work you throw
 44 | # at it, the wider the performance gap.  since trivial
 45 | # inputs are already fast enough, obviously we go with
 46 | # the multisplit2 approach.
 47 | #
 48 | 
 49 | def multisplit1(s, separators):
 50 |     """
 51 |     Like str.split(), but separators is an iterable
 52 |     of strings to separate on.  (separators can be a
 53 |     string, in which case multisplit separates on each
 54 |     character.)
 55 | 
 56 |     multisplit('ab:cd,ef', ':,') => ["ab", "cd", "ef"]
 57 |     """
 58 |     if not s or not separators:
 59 |         return [s]
 60 |     if len(separators) == 1:
 61 |         return s.split(separators[0])
 62 |     splits = []
 63 |     while s:
 64 |         candidates = []
 65 |         for separator in separators:
 66 |             split, found, trailing = s.partition(separator)
 67 |             if found:
 68 |                 candidates.append((len(split), split, trailing))
 69 |         if not candidates:
 70 |             break
 71 |         candidates.sort()
 72 |         _, fragment, s = candidates[0]
 73 |         splits.append(fragment)
 74 |     splits.append(s)
 75 |     return splits
 76 | 
 77 | 
 78 | def multisplit2(s, separators):
 79 |     """
 80 |     Like str.split(), but separators is an iterable of strings to separate on.
 81 |     (If separators is a string, multisplit separates on each character
 82 |     separately in the string.)
 83 | 
 84 |     Returns a list of the substrings, split by the separators.
 85 | 
 86 |     Example:
 87 |         multisplit('ab:cd,ef', ':,')
 88 |     returns
 89 |         ["ab", "cd", "ef"]
 90 |     """
 91 |     if not s or not separators:
 92 |         return [s]
 93 | 
 94 |     # candidates is a sorted list of lists.  each sub-list contains:
 95 |     #   [ first_appearance_index, separator_string, len_separator ]
 96 |     # since the lowest appearance is sorted first, you simply split
 97 |     # off based on candidates[0], shift all the first_appearance_indexes
 98 |     # down by how much you lopped off, recompute the first_appearance_index
 99 |     # of candidate[0]'s separator_string, re-sort, and do it again.
100 |     # (you remove a candidate when there are no more appearances in
101 |     # the string--when "".find returns -1.)
102 |     candidates = []
103 |     for separator in separators:
104 |         index = s.find(separator)
105 |         if index != -1:
106 |             candidates.append([index, separator, len(separator)])
107 | 
108 |     if not candidates:
109 |         return [s]
110 |     if len(candidates) == 1:
111 |         return s.split(candidates[0])
112 | 
113 |     candidates.sort()
114 |     splits = []
115 | 
116 |     while True:
117 |         assert s
118 |         # print(f"\n{s=}\n{candidates=}\n{splits=}")
119 |         index, separator, len_separator = candidates[0]
120 |         segment = s[:index]
121 |         splits.append(segment)
122 |         new_start = index + len_separator
123 |         s = s[new_start:]
124 |         # print(f"{new_start=} new {s=}")
125 |         for candidate in candidates:
126 |             candidate[0] -= new_start
127 |         index = s.find(separator)
128 |         if index < 0:
129 |             # there aren't any more appearances of candidate[0].
130 |             if len(candidates) == 2:
131 |                 # once we remove candidate[0],
132 |                 # we'll only have one candidate separator left.
133 |                 # so just split normally on that separator and exit.
134 |                 #
135 |                 # note: this is the only way to exit the loop.
136 |                 # we always reach it, because at some point
137 |                 # there's only one candidate left.
138 |                 splits.extend(s.split(candidates[1][1]))
139 |                 break
140 |             candidates.pop(0)
141 |         else:
142 |             candidates[0][0] = index
143 |             candidates.sort()
144 | 
145 |     # print(f"\nfinal {splits=}")
146 |     return splits
147 | 
148 | 
149 | 
150 | with open("alice.in.wonderland.txt", "rt", encoding="utf-8") as f:
151 |     text = f.read()
152 | 
153 | separators = " ,:\n"
154 | result1 = multisplit1(text, separators)
155 | result2 = multisplit2(text, separators)
156 | 
157 | assert result1==result2
158 | 
159 | import timeit
160 | 
161 | for text, separators, number in (
162 |     ('ab:cd,ef', ':,', 1000000),
163 |     ('abWWcdXXcdYYabZZab', ('ab', 'cd'), 1000000),
164 |     (text[:5000], separators, 1000),
165 |     ):
166 |     result1 = multisplit1(text, separators)
167 |     result2 = multisplit2(text, separators)
168 |     assert result1==result2
169 | 
170 |     if len(text) > 30:
171 |         summary = repr(text[:25] + "[...]")
172 |     else:
173 |         summary = repr(text)
174 |     print(f"text={summary} {separators=}")
175 |     result1 = timeit.timeit("multisplit1(text, separators)", globals=globals(), number=number)
176 |     result2 = timeit.timeit("multisplit2(text, separators)", globals=globals(), number=number)
177 |     print(f"    multisplit1: {number} executions took {result1}s")
178 |     print(f"    multisplit2: {number} executions took {result2}s")
179 |     print()
180 | 
181 | 


--------------------------------------------------------------------------------
/resources/images/big.header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larryhastings/big/d91ffb12841e5970cf07262e9d7ecbe37bdc8b91/resources/images/big.header.png


--------------------------------------------------------------------------------
/resources/images/big.package.final.3.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larryhastings/big/d91ffb12841e5970cf07262e9d7ecbe37bdc8b91/resources/images/big.package.final.3.xcf


--------------------------------------------------------------------------------
/resources/unicode/parse_ucd_xml.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | 
  3 | #
  4 | # This script parses the Unicode database,
  5 | # specifically the XML version.
  6 | #
  7 | # To run this script, you'll need to locally install
  8 | # the Unicode XML database.
  9 | #
 10 | #     Go to this URL:
 11 | #            https://www.unicode.org/Public/
 12 | #     Then click on the directory for the version you want,
 13 | #     probably the current version.  As of this writing the
 14 | #     current version is 15.1.0.
 15 | #
 16 | #     Then click on the "ucdxml" directory.
 17 | #
 18 | #     Download the "ucd.all.flat.zip" file  and unzip it.
 19 | #     It should only contain one file, "ucd.all.flat.xml".
 20 | #     Copy that file into the same directory as this script.
 21 | #
 22 | 
 23 | # If you do that, you should see this output:
 24 | expected_output = """
 25 | A canonical list of all Unicode whitespace and line-breaking characters.
 26 | 
 27 | Unicode whitespace:
 28 | 
 29 |     char    decimal  unicode  description
 30 |     --------------------------------------------------
 31 |     '\t'          9   U+0009  CHARACTER TABULATION
 32 |     '\n'         10   U+000A  LINE FEED (LF)
 33 |     '\v'         11   U+000B  LINE TABULATION
 34 |     '\f'         12   U+000C  FORM FEED (FF)
 35 |     '\r'         13   U+000D  CARRIAGE RETURN (CR)
 36 |     ' '          32   U+0020  SPACE
 37 |     '\x85'      133   U+0085  NEXT LINE (NEL)
 38 |     '\xa0'      160   U+00A0  NON-BREAKING SPACE
 39 |     '\u1680'   5760   U+1680  OGHAM SPACE MARK
 40 |     '\u2000'   8192   U+2000  EN QUAD
 41 |     '\u2001'   8193   U+2001  EM QUAD
 42 |     '\u2002'   8194   U+2002  EN SPACE
 43 |     '\u2003'   8195   U+2003  EM SPACE
 44 |     '\u2004'   8196   U+2004  THREE-PER-EM SPACE
 45 |     '\u2005'   8197   U+2005  FOUR-PER-EM SPACE
 46 |     '\u2006'   8198   U+2006  SIX-PER-EM SPACE
 47 |     '\u2007'   8199   U+2007  FIGURE SPACE
 48 |     '\u2008'   8200   U+2008  PUNCTUATION SPACE
 49 |     '\u2009'   8201   U+2009  THIN SPACE
 50 |     '\u200a'   8202   U+200A  HAIR SPACE
 51 |     '\u2028'   8232   U+2028  LINE SEPARATOR
 52 |     '\u2029'   8233   U+2029  PARAGRAPH SEPARATOR
 53 |     '\u202f'   8239   U+202F  NARROW NO-BREAK SPACE
 54 |     '\u205f'   8287   U+205F  MEDIUM MATHEMATICAL SPACE
 55 |     '\u3000'  12288   U+3000  IDEOGRAPHIC SPACE
 56 | 
 57 | Unicode line-breaking whitespace:
 58 | 
 59 |     char    decimal  unicode  description
 60 |     --------------------------------------------------
 61 |     '\n'         10   U+000A  LINE FEED (LF)
 62 |     '\v'         11   U+000B  LINE TABULATION
 63 |     '\f'         12   U+000C  FORM FEED (FF)
 64 |     '\r'         13   U+000D  CARRIAGE RETURN (CR)
 65 |     '\x85'      133   U+0085  NEXT LINE (NEL)
 66 |     '\u2028'   8232   U+2028  LINE SEPARATOR
 67 |     '\u2029'   8233   U+2029  PARAGRAPH SEPARATOR
 68 | 
 69 | -----------------------------------------------------------------
 70 | 
 71 | There are no characters considered to be whitespace by Unicode but not by the Python str object.
 72 | 
 73 | These characters are considered to be whitespace by the Python str object, but not by Unicode:
 74 | 
 75 |     char    decimal  unicode  description
 76 |     --------------------------------------------------
 77 |     '\x1c'       28   U+001C  INFORMATION SEPARATOR FOUR
 78 |     '\x1d'       29   U+001D  INFORMATION SEPARATOR THREE
 79 |     '\x1e'       30   U+001E  INFORMATION SEPARATOR TWO
 80 |     '\x1f'       31   U+001F  INFORMATION SEPARATOR ONE
 81 | 
 82 | ASCII and the Python bytes object agree on the list of whitespace characters.
 83 | 
 84 | There are no characters considered to be line-breaking whitespace by Unicode but not by the Python str object.
 85 | 
 86 | These characters are considered to be line-breaking whitespace by the Python str object, but not by Unicode:
 87 | 
 88 |     char    decimal  unicode  description
 89 |     --------------------------------------------------
 90 |     '\x1c'       28   U+001C  INFORMATION SEPARATOR FOUR
 91 |     '\x1d'       29   U+001D  INFORMATION SEPARATOR THREE
 92 |     '\x1e'       30   U+001E  INFORMATION SEPARATOR TWO
 93 | 
 94 | These characters are considered to be line-breaking whitespace by ASCII, but not by the Python bytes object:
 95 | 
 96 |     char    decimal  unicode  description
 97 |     --------------------------------------------------
 98 |     '\v'         11   U+000B  LINE TABULATION
 99 |     '\f'         12   U+000C  FORM FEED (FF)
100 | 
101 | There are no characters considered to be line-breaking whitespace by the Python bytes object but not by ASCII.
102 | """
103 | 
104 | 
105 | 
106 | import xml.etree.ElementTree as et
107 | 
108 | ucd = et.parse("ucd.all.flat.xml").getroot()
109 | 
110 | #
111 | # lb definitions gleaned from:
112 | #   Unicode Standard Annex #14
113 | #   UNICODE LINE BREAKING ALGORITHM
114 | #
115 | #   https://www.unicode.org/reports/tr14/tr14-32.html
116 | #
117 | lb_linebreaks = {
118 |     'BK': 'Mandatory Break - Cause a line break (after)',
119 |     'CR': 'Carriage Return - Cause a line break (after), except between CR and LF',
120 |     'LF': 'Line Feed - Cause a line break (after)',
121 |     'NL': 'Next Line - Cause a line break (after)',
122 | }
123 | 
124 | lb_not_linebreaks = {
125 |     'CM': 'Combining Mark - Prohibit a line break between the character and the preceding character',
126 |     'SG': 'Surrogate - Do not occur in well-formed text',
127 |     'WJ': 'Word Joiner - Prohibit line breaks before and after',
128 |     'ZW': 'Zero Width Space - Provide a break opportunity',
129 |     'GL': 'Non-breaking ("Glue") - Prohibit line breaks before and after',
130 |     'SP': 'Space - Enable indirect line breaks',
131 | 
132 |     'B2': 'Break Opportunity Before and After - Provide a line break opportunity before and after the character',
133 |     'BA': 'Break After - Generally provide a line break opportunity after the character',
134 |     'BB': 'Break Before - Punctuation used in dictionaries    Generally provide a line break opportunity before the character',
135 |     'HY': 'Hyphen - Provide a line break opportunity after the character, except in numeric context',
136 |     'CB': 'Contingent Break Opportunity -Provide a line break opportunity contingent on additional information',
137 |     }
138 | 
139 | # The Python parser understands \v for vertical tab and \f for form feed.
140 | # But the repr for str objects doesn't convert back to those.
141 | # I prefer lookin' at 'em, and it's my script, so I do this conversion myself.
142 | special_char_reprs = {
143 |     "\x0b": "'\\v'",
144 |     "\x0c": "'\\f'",
145 |     }
146 | 
147 | def special_char_repr(c):
148 |     return special_char_reprs.get(c, repr(c))
149 | 
150 | unicode_whitespace = []
151 | unicode_linebreaks = []
152 | 
153 | ascii_whitespace = []
154 | ascii_linebreaks = []
155 | 
156 | str_whitespace = []
157 | str_linebreaks = []
158 | 
159 | bytes_whitespace = []
160 | bytes_linebreaks = []
161 | 
162 | 
163 | for item in ucd.iter():
164 |     tag = item.tag.rpartition('}')[2]
165 |     if tag != 'char':
166 |         continue
167 | 
168 |     code_point = item.attrib.get('cp')
169 |     if not code_point:
170 |         # not a character.
171 |         # the database has other entries, e.g. ranges (defined via "first-cp" through "last-cp")
172 |         continue
173 | 
174 |     lb = item.attrib['lb']
175 |     description = item.attrib['na1'] or item.attrib['na']
176 | 
177 |     i = int(code_point.lstrip('0') or '0', 16)
178 |     c = chr(i)
179 |     repr_c = special_char_repr(c).ljust(8)
180 |     formatted = f"    {repr_c} {i:>6}   U+{code_point}  {description}"
181 |     t = (i, formatted)
182 | 
183 | 
184 |     is_unicode_whitespace = item.attrib['WSpace'] == 'Y'
185 |     is_unicode_linebreak = lb_linebreaks.get(lb)
186 | 
187 |     if is_unicode_whitespace:
188 |         unicode_whitespace.append(t)
189 |         if is_unicode_linebreak:
190 |             unicode_linebreaks.append(t)
191 |     else:
192 |         assert not is_unicode_linebreak, "Unicode says this character is a linebreak but not whitespace!{formatted}"
193 | 
194 | 
195 |     test_string = f"a{c}b"
196 |     is_str_whitespace = len(test_string.split()) == 2
197 |     is_str_linebreak = len(test_string.splitlines()) == 2
198 | 
199 |     if is_str_whitespace:
200 |         str_whitespace.append(t)
201 |         if is_str_linebreak:
202 |             str_linebreaks.append(t)
203 |     else:
204 |         assert not is_str_linebreak, "The Python str object says this character is a linebreak but not whitespace!{formatted}"
205 | 
206 | 
207 |     defined_for_ascii = i < 128
208 |     if defined_for_ascii:
209 |         test_bytes = test_string.encode('ascii')
210 |         is_bytes_whitespace = len(test_bytes.split()) == 2
211 |         is_bytes_linebreak = len(test_bytes.splitlines()) == 2
212 |         if is_bytes_whitespace:
213 |             bytes_whitespace.append(t)
214 |             if is_bytes_linebreak:
215 |                 bytes_linebreaks.append(t)
216 |         else:
217 |             assert not is_bytes_linebreak, "The Python bytes object says this character is a linebreak but not whitespace!{formatted}"
218 | 
219 |         if is_unicode_whitespace:
220 |             ascii_whitespace.append(t)
221 |             if is_unicode_linebreak:
222 |                 ascii_linebreaks.append(t)
223 | 
224 | assert unicode_whitespace
225 | assert unicode_linebreaks
226 | assert ascii_whitespace
227 | assert ascii_linebreaks
228 | assert str_whitespace
229 | assert str_linebreaks
230 | assert bytes_whitespace
231 | assert bytes_linebreaks
232 | 
233 | ##############################################################################
234 | 
235 | print("A canonical list of all Unicode whitespace and line-breaking characters.")
236 | print()
237 | 
238 | heading = """
239 |     char    decimal  unicode  description
240 |     --------------------------------------------------
241 | """.rstrip('\n')
242 | 
243 | print("Unicode whitespace:")
244 | print(heading)
245 | for i, s in unicode_whitespace:
246 |     print(s)
247 | 
248 | print()
249 | 
250 | print("Unicode line-breaking whitespace:")
251 | print(heading)
252 | for i, s in unicode_linebreaks:
253 |     print(s)
254 | print()
255 | 
256 | print("-" * 65)
257 | print()
258 | 
259 | for what, first, first_list, second, second_list in (
260 |     (
261 |         "whitespace",
262 |         "Unicode", unicode_whitespace,
263 |         "the Python str object", str_whitespace,
264 |         ),
265 |     (
266 |         "whitespace",
267 |         "ASCII", ascii_whitespace,
268 |         "the Python bytes object", bytes_whitespace,
269 |         ),
270 |     (
271 |         "line-breaking whitespace",
272 |         "Unicode", unicode_linebreaks,
273 |         "the Python str object", str_linebreaks,
274 |         ),
275 |     (
276 |         "line-breaking whitespace",
277 |         "ASCII", ascii_linebreaks,
278 |         "the Python bytes object", bytes_linebreaks,
279 |         ),
280 |     ):
281 | 
282 |     first_set = set(first_list)
283 |     second_set = set(second_list)
284 | 
285 |     if first_set == second_set:
286 |         print(f"{first[0].upper()}{first[1:]} and {second} agree on the list of {what} characters.")
287 |         print()
288 |     else:
289 |         for (a, a_set, b, b_set) in (
290 |             (first, first_set, second, second_set),
291 |             (second, second_set, first, first_set),
292 |             ):
293 |             delta = list(a_set - b_set)
294 |             if not delta:
295 |                 print(f"There are no characters considered to be {what} by {a} but not by {b}.")
296 |             else:
297 |                 delta.sort()
298 |                 print(f"These characters are considered to be {what} by {a}, but not by {b}:")
299 |                 print(heading)
300 |                 for i, s in delta:
301 |                     print(s)
302 |             print()
303 | 


--------------------------------------------------------------------------------
/tests/bigtestlib.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | 
  3 | _license = """
  4 | big
  5 | Copyright 2022-2024 Larry Hastings
  6 | All rights reserved.
  7 | 
  8 | Permission is hereby granted, free of charge, to any person obtaining a
  9 | copy of this software and associated documentation files (the "Software"),
 10 | to deal in the Software without restriction, including without limitation
 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
 12 | and/or sell copies of the Software, and to permit persons to whom the
 13 | Software is furnished to do so, subject to the following conditions:
 14 | 
 15 | The above copyright notice and this permission notice shall be included
 16 | in all copies or substantial portions of the Software.
 17 | 
 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 25 | """
 26 | 
 27 | import io
 28 | import pathlib
 29 | import unittest
 30 | 
 31 | stats = {
 32 |     "failures": 0,
 33 |     "errors": 0,
 34 |     "expected failures": 0,
 35 |     "unexpected successes": 0,
 36 |     "skipped": 0,
 37 |     }
 38 | 
 39 | attribute_names = {
 40 |     "failures": "failures",
 41 |     "errors": "errors",
 42 |     "expected failures": "expectedFailures",
 43 |     "unexpected successes": "unexpectedSuccesses",
 44 |     "skipped": "skipped",
 45 |     }
 46 | 
 47 | def preload_local_big():
 48 |     """
 49 |     Pre-load the local "big" module, to preclude finding
 50 |     an already-installed one on the path.
 51 |     """
 52 |     import pathlib
 53 |     import sys
 54 | 
 55 |     argv_0 = pathlib.Path(sys.argv[0])
 56 |     big_dir = argv_0.resolve().parent
 57 |     while True:
 58 |         big_init = big_dir / "big" / "__init__.py"
 59 |         if big_init.is_file():
 60 |             break
 61 |         big_dir = big_dir.parent
 62 | 
 63 |     # this almost certainly *is* a git checkout
 64 |     # ... but that's not required, so don't assert it.
 65 |     # assert (big_dir / ".git" / "config").is_file()
 66 | 
 67 |     if big_dir not in sys.path:
 68 |         sys.path.insert(1, str(big_dir))
 69 | 
 70 |     import big
 71 |     assert big.__file__.startswith(str(big_dir))
 72 |     return big_dir
 73 | 
 74 | 
 75 | def run(name, module, permutations=None):
 76 |     if name:
 77 |         print(f"Testing {name}...")
 78 | 
 79 |     # this is a lot of work to suppress the "\nOK"!
 80 |     sio = io.StringIO()
 81 |     runner = unittest.TextTestRunner(stream=sio)
 82 |     t = unittest.main(module=module, exit=False, testRunner=runner)
 83 |     result = t.result
 84 |     for name in stats:
 85 |         value = getattr(result, attribute_names[name], ())
 86 |         stats[name] += len(value)
 87 | 
 88 |     for line in sio.getvalue().split("\n"):
 89 |         if not line.startswith("Ran"):
 90 |             print(line)
 91 |             continue
 92 | 
 93 |         if permutations:
 94 |             fields = line.split()
 95 |             tests = fields[2]
 96 |             assert tests in ("test", "tests"), f"expected fields[2] to be 'test' or 'tests', but fields={fields}"
 97 |             fields[2] = f"{tests}, with {permutations()} total permutations,"
 98 |             line = " ".join(fields)
 99 |         print(line)
100 |         print()
101 |         # prevent printing the  \n and OK
102 |         break
103 | 
104 | def finish():
105 |     if not (stats['failures'] or stats['errors']):
106 |         result = "OK"
107 |     else: # pragma: no cover
108 |         result = "FAILED"
109 | 
110 |     fields = [f"{name}={value}" for name, value in stats.items() if value]
111 |     if fields: # pragma: no cover
112 |         addendum = ", ".join(fields)
113 |         result = f"{result} ({addendum})"
114 |     print(result)
115 | 


--------------------------------------------------------------------------------
/tests/grepfile:
--------------------------------------------------------------------------------
 1 | aaaa
 2 | bbbb
 3 | abc
 4 | cccc
 5 | ddddd
 6 | AAAA
 7 | BBBB
 8 | ABC
 9 | CCCC
10 | DDDDD
11 | 


--------------------------------------------------------------------------------
/tests/test_all.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python3
 2 | 
 3 | _license = """
 4 | big
 5 | Copyright 2022-2024 Larry Hastings
 6 | All rights reserved.
 7 | 
 8 | Permission is hereby granted, free of charge, to any person obtaining a
 9 | copy of this software and associated documentation files (the "Software"),
10 | to deal in the Software without restriction, including without limitation
11 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 | and/or sell copies of the Software, and to permit persons to whom the
13 | Software is furnished to do so, subject to the following conditions:
14 | 
15 | The above copyright notice and this permission notice shall be included
16 | in all copies or substantial portions of the Software.
17 | 
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 | """
26 | 
27 | import bigtestlib
28 | bigtestlib.preload_local_big()
29 | 
30 | 
31 | for test_module in """
32 |     test_boundinnerclass
33 |     test_builtin
34 |     test_deprecated
35 |     test_file
36 |     test_graph
37 |     test_heap
38 |     test_itertools
39 |     test_log
40 |     test_metadata
41 |     test_scheduler
42 |     test_state
43 |     test_text
44 |     test_time
45 |     test_version
46 | """.strip().split():
47 |     module = __import__(test_module)
48 |     module.run_tests()
49 | 
50 | bigtestlib.finish()
51 | 


--------------------------------------------------------------------------------
/tests/test_boundinnerclass.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | 
  3 | _license = """
  4 | big
  5 | Copyright 2022-2024 Larry Hastings
  6 | All rights reserved.
  7 | 
  8 | Permission is hereby granted, free of charge, to any person obtaining a
  9 | copy of this software and associated documentation files (the "Software"),
 10 | to deal in the Software without restriction, including without limitation
 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
 12 | and/or sell copies of the Software, and to permit persons to whom the
 13 | Software is furnished to do so, subject to the following conditions:
 14 | 
 15 | The above copyright notice and this permission notice shall be included
 16 | in all copies or substantial portions of the Software.
 17 | 
 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 25 | """
 26 | 
 27 | import bigtestlib
 28 | bigtestlib.preload_local_big()
 29 | 
 30 | import big.all as big
 31 | from big.boundinnerclass import *
 32 | import itertools
 33 | import re
 34 | import unittest
 35 | 
 36 | 
 37 | class Outer(object):
 38 |     @BoundInnerClass
 39 |     class Inner(object):
 40 |         def __init__(self, outer):
 41 |             self.outer = outer
 42 | 
 43 |     @BoundInnerClass
 44 |     class SubclassOfInner(Inner.cls):
 45 |         def __init__(self, outer):
 46 |             super().__init__()
 47 |             assert self.outer == outer
 48 | 
 49 |     @BoundInnerClass
 50 |     class SubsubclassOfInner(SubclassOfInner.cls):
 51 |         def __init__(self, outer):
 52 |             super().__init__()
 53 |             assert self.outer == outer
 54 | 
 55 |     @BoundInnerClass
 56 |     class Subclass2OfInner(Inner.cls):
 57 |         def __init__(self, outer):
 58 |             super().__init__()
 59 |             assert self.outer == outer
 60 | 
 61 |     class RandomUnboundInner(object):
 62 |         def __init__(self):
 63 |             super().__init__()
 64 |             pass
 65 | 
 66 |     @BoundInnerClass
 67 |     class MultipleInheritanceTest(SubclassOfInner.cls,
 68 |                  RandomUnboundInner,
 69 |                  Subclass2OfInner.cls):
 70 |         def __init__(self, outer, unittester):
 71 |             super().__init__()
 72 |             unittester.assertEqual(self.outer, outer)
 73 | 
 74 |     @UnboundInnerClass
 75 |     class UnboundSubclassOfInner(Inner.cls):
 76 |         pass
 77 | 
 78 |     @UnboundInnerClass
 79 |     class SubclassOfUnboundSubclassOfInner(UnboundSubclassOfInner.cls):
 80 |         pass
 81 | 
 82 | 
 83 | class BigBoundInnerClassesTests(unittest.TestCase):
 84 | 
 85 |     def test_cached(self):
 86 |         outer = Outer()
 87 |         inner1 = outer.Inner
 88 |         inner2 = outer.Inner
 89 |         self.assertIs(inner1, inner2)
 90 | 
 91 |     def test_permutations(self):
 92 |         def test_permutation(ordering):
 93 |             outer = Outer()
 94 |             # This strange "for" statement lets us test every possible order of
 95 |             # initialization for the "inner" / "subclass" / "subsubclass" objects.
 96 |             # We always iterate over all three numbers, but in a different order each time.
 97 |             for which in ordering:
 98 |                 if   which == 1: inner = outer.Inner()
 99 |                 elif which == 2: subclass = outer.SubclassOfInner()
100 |                 elif which == 3: subsubclass = outer.SubsubclassOfInner()
101 | 
102 |             self.assertEqual(outer.Inner, outer.Inner)
103 |             self.assertIsInstance(inner, outer.Inner)
104 |             self.assertIsInstance(inner, Outer.Inner)
105 | 
106 |             self.assertIsInstance(subclass, Outer.SubclassOfInner)
107 |             self.assertIsInstance(subclass, outer.SubclassOfInner)
108 |             self.assertIsInstance(subclass, Outer.Inner)
109 |             self.assertIsInstance(subclass, outer.Inner)
110 | 
111 |             self.assertIsInstance(subsubclass, Outer.SubsubclassOfInner)
112 |             self.assertIsInstance(subsubclass, outer.SubsubclassOfInner)
113 |             self.assertIsInstance(subsubclass, Outer.SubclassOfInner)
114 |             self.assertIsInstance(subsubclass, outer.SubclassOfInner)
115 |             self.assertIsInstance(subsubclass, Outer.Inner)
116 |             self.assertIsInstance(subsubclass, outer.Inner)
117 | 
118 |         for ordering in itertools.permutations([1, 2, 3]):
119 |             test_permutation(ordering)
120 | 
121 |     def test_multiple_inheritance(self):
122 |         outer = Outer()
123 |         multiple_inheritance_test = outer.MultipleInheritanceTest(self)
124 |         self.assertEqual(outer.MultipleInheritanceTest.mro(),
125 |             [
126 |             # bound inner class, notice lowercase-o "outer"
127 |             outer.MultipleInheritanceTest,
128 |             # unbound inner class, notice uppercase-o "Outer"
129 |             Outer.MultipleInheritanceTest,
130 |             outer.SubclassOfInner, # bound
131 |             Outer.SubclassOfInner, # unbound
132 |             Outer.RandomUnboundInner, # etc.
133 |             outer.Subclass2OfInner,
134 |             Outer.Subclass2OfInner,
135 |             outer.Inner,
136 |             Outer.Inner,
137 |             object
138 |             ]
139 |             )
140 | 
141 |     def test_unbound_subclass_of_inner_class(self):
142 |         outer = Outer()
143 |         UnboundSubclassOfInner = outer.UnboundSubclassOfInner
144 |         self.assertEqual(UnboundSubclassOfInner.mro(),
145 |             [
146 |             outer.UnboundSubclassOfInner,
147 |             Outer.UnboundSubclassOfInner,
148 |             outer.Inner,
149 |             Outer.Inner,
150 |             object
151 |             ]
152 |             )
153 | 
154 |         SubclassOfUnboundSubclassOfInner = outer.SubclassOfUnboundSubclassOfInner
155 |         self.assertEqual(SubclassOfUnboundSubclassOfInner.mro(),
156 |             [
157 |             outer.SubclassOfUnboundSubclassOfInner,
158 |             Outer.SubclassOfUnboundSubclassOfInner,
159 |             outer.UnboundSubclassOfInner,
160 |             Outer.UnboundSubclassOfInner,
161 |             outer.Inner,
162 |             Outer.Inner,
163 |             object,
164 |             ]
165 |             )
166 | 
167 |     def test_inner_child(self):
168 |         outer = Outer()
169 |         class InnerChild(outer.Inner):
170 |             pass
171 | 
172 |         inner_child = InnerChild()
173 | 
174 |         self.assertIsInstance(inner_child, Outer.Inner)
175 |         self.assertIsInstance(inner_child, InnerChild)
176 |         self.assertIsInstance(inner_child, outer.Inner)
177 | 
178 |         repr_re = re.compile(r"^<[A-Za-z0-9_]+\.InnerChild object bound to <[A-Za-z0-9_]+\.Outer object at 0x")
179 |         match = repr_re.match(repr(inner_child))
180 |         self.assertTrue(match)
181 | 
182 |     def test_outer_evaluates_to_false(self):
183 |         """
184 |         Regression test for a bugfix!
185 | 
186 |         The behavior:
187 | 
188 |             If you have an outer class Outer, and it contains an
189 |             inner class Inner decorated with @BoundInnerClass,
190 |             and o is an instance of Outer, and o evaluates to
191 |             false in a boolean context,
192 |             o.Inner would be the *unbound* version of Inner.
193 | 
194 |         The bug:
195 |             When you access a class descriptor--an object that
196 |             implements __get__--the parameters are contingent
197 |             on how you were accessed.
198 | 
199 |             If you access Inner through an instance, e.g. o.Inner,
200 |             the descriptor is called with the instance:
201 |                 __get__(self, o, Outer)
202 | 
203 |             If you access Inner through the class itself,
204 |             e.g. Outer.Inner, the descriptor is called with None
205 |             instead of the instance:
206 |                 __get__(self, None, Outer)
207 | 
208 |             See
209 |                 https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance
210 |             vs.
211 |                 https://docs.python.org/3/howto/descriptor.html#invocation-from-a-class
212 | 
213 |             The bug was, I wasn't checking to see if obj was None,
214 |             I was checking to see if obj was false.  Oops.
215 | 
216 |         The fix:
217 |             Easy.  Use "if obj is None" instead of "if not obj".
218 | 
219 |         -----
220 | 
221 |         This bug is thirteen years old!
222 |         It's in the second revision of the Bound Inner Classes
223 |         recipe I posted to the good ol' cookbook:
224 | 
225 |             https://code.activestate.com/recipes/577070-bound-inner-classes/history/2/
226 | 
227 |         Before I made "big", it was inconvenient for me
228 |         to use BoundInnerClasses.  So I rarely used them.
229 |         I guess I just didn't put enough CPU seconds
230 |         through the class to stumble over this bug before.
231 |         """
232 | 
233 |         class Outer:
234 |             def __bool__(self):
235 |                 return False
236 | 
237 |             @BoundInnerClass
238 |             class Inner:
239 |                 def __init__(self, outer):
240 |                     self.outer = outer
241 | 
242 |         o = Outer()
243 |         self.assertFalse(o)
244 |         self.assertNotEqual(Outer.Inner, o.Inner)
245 |         i = o.Inner()
246 |         self.assertEqual(o, i.outer)
247 | 
248 | 
249 | def run_tests():
250 |     bigtestlib.run(name="big.boundinnerclass", module=__name__)
251 | 
252 | if __name__ == "__main__": # pragma: no cover
253 |     run_tests()
254 |     bigtestlib.finish()
255 | 


--------------------------------------------------------------------------------
/tests/test_builtin.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | 
  3 | _license = """
  4 | big
  5 | Copyright 2022-2024 Larry Hastings
  6 | All rights reserved.
  7 | 
  8 | Permission is hereby granted, free of charge, to any person obtaining a
  9 | copy of this software and associated documentation files (the "Software"),
 10 | to deal in the Software without restriction, including without limitation
 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
 12 | and/or sell copies of the Software, and to permit persons to whom the
 13 | Software is furnished to do so, subject to the following conditions:
 14 | 
 15 | The above copyright notice and this permission notice shall be included
 16 | in all copies or substantial portions of the Software.
 17 | 
 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 25 | """
 26 | 
 27 | import bigtestlib
 28 | bigtestlib.preload_local_big()
 29 | 
 30 | import big.all as big
 31 | import unittest
 32 | 
 33 | 
 34 | class BigTests(unittest.TestCase):
 35 | 
 36 |     def test_try_int(self):
 37 |         self.assertTrue(big.try_int(0))
 38 |         self.assertTrue(big.try_int(0.0))
 39 |         self.assertTrue(big.try_int("0"))
 40 | 
 41 |         self.assertFalse(big.try_int(3j))
 42 |         self.assertFalse(big.try_int(None))
 43 |         self.assertFalse(big.try_int(()))
 44 |         self.assertFalse(big.try_int({}))
 45 |         self.assertFalse(big.try_int([]))
 46 |         self.assertFalse(big.try_int(set()))
 47 |         self.assertFalse(big.try_int("0.0"))
 48 |         self.assertFalse(big.try_int("abc"))
 49 |         self.assertFalse(big.try_int("3j"))
 50 |         self.assertFalse(big.try_int("None"))
 51 | 
 52 |     def test_try_float(self):
 53 |         self.assertTrue(big.try_float(0))
 54 |         self.assertTrue(big.try_float(0.0))
 55 |         self.assertTrue(big.try_float("0"))
 56 |         self.assertTrue(big.try_float("0.0"))
 57 | 
 58 |         self.assertFalse(big.try_float(3j))
 59 |         self.assertFalse(big.try_float(None))
 60 |         self.assertFalse(big.try_float(()))
 61 |         self.assertFalse(big.try_float({}))
 62 |         self.assertFalse(big.try_float([]))
 63 |         self.assertFalse(big.try_float(set()))
 64 |         self.assertFalse(big.try_float("abc"))
 65 |         self.assertFalse(big.try_float("3j"))
 66 |         self.assertFalse(big.try_float("None"))
 67 | 
 68 |     def test_get_int(self):
 69 |         sentinel = object()
 70 |         self.assertEqual(big.get_int(0,      sentinel), 0)
 71 |         self.assertEqual(big.get_int(0.0,    sentinel), 0)
 72 |         self.assertEqual(big.get_int("0",    sentinel), 0)
 73 | 
 74 |         self.assertEqual(big.get_int(35,     sentinel), 35)
 75 |         self.assertEqual(big.get_int(35.1,   sentinel), 35)
 76 | 
 77 |         self.assertEqual(big.get_int(3j,     sentinel), sentinel)
 78 |         self.assertEqual(big.get_int(None,   sentinel), sentinel)
 79 |         self.assertEqual(big.get_int((),     sentinel), sentinel)
 80 |         self.assertEqual(big.get_int({},     sentinel), sentinel)
 81 |         self.assertEqual(big.get_int([],     sentinel), sentinel)
 82 |         self.assertEqual(big.get_int(set(),  sentinel), sentinel)
 83 |         self.assertEqual(big.get_int("",     sentinel), sentinel)
 84 |         self.assertEqual(big.get_int("0.0",  sentinel), sentinel)
 85 |         self.assertEqual(big.get_int("abc",  sentinel), sentinel)
 86 |         self.assertEqual(big.get_int("3j",   sentinel), sentinel)
 87 |         self.assertEqual(big.get_int("None", sentinel), sentinel)
 88 | 
 89 |         d = {}
 90 |         self.assertEqual(big.get_int(d), d)
 91 |         self.assertEqual(big.get_int(3j), 3j)
 92 |         self.assertEqual(big.get_int(None), None)
 93 |         self.assertEqual(big.get_int("abc"), "abc")
 94 | 
 95 |     def test_get_float(self):
 96 |         sentinel = object()
 97 |         self.assertEqual(big.get_float(0,      sentinel), 0.0)
 98 |         self.assertEqual(big.get_float(0.0,    sentinel), 0.0)
 99 |         self.assertEqual(big.get_float("0",    sentinel), 0.0)
100 |         self.assertEqual(big.get_float("0.0",  sentinel), 0.0)
101 | 
102 |         self.assertEqual(big.get_float(35,     sentinel), 35.0)
103 |         self.assertEqual(big.get_float(35.1,   sentinel), 35.1)
104 | 
105 |         self.assertEqual(big.get_float(3j,     sentinel), sentinel)
106 |         self.assertEqual(big.get_float(None,   sentinel), sentinel)
107 |         self.assertEqual(big.get_float((),     sentinel), sentinel)
108 |         self.assertEqual(big.get_float({},     sentinel), sentinel)
109 |         self.assertEqual(big.get_float([],     sentinel), sentinel)
110 |         self.assertEqual(big.get_float(set(),  sentinel), sentinel)
111 |         self.assertEqual(big.get_float("",     sentinel), sentinel)
112 |         self.assertEqual(big.get_float("abc",  sentinel), sentinel)
113 |         self.assertEqual(big.get_float("3j",   sentinel), sentinel)
114 |         self.assertEqual(big.get_float("None", sentinel), sentinel)
115 | 
116 |         d = {}
117 |         self.assertEqual(big.get_float(d), d)
118 |         self.assertEqual(big.get_float(3j), 3j)
119 |         self.assertEqual(big.get_float(None), None)
120 |         self.assertEqual(big.get_float("abc"), "abc")
121 | 
122 |     def test_get_int_or_float(self):
123 |         sentinel = object()
124 |         self.assertEqual(     big.get_int_or_float(0,       sentinel), 0)
125 |         self.assertIsInstance(big.get_int_or_float(0,       sentinel), int)
126 |         self.assertEqual(     big.get_int_or_float("0",     sentinel), 0)
127 |         self.assertIsInstance(big.get_int_or_float("0",     sentinel), int)
128 | 
129 |         self.assertEqual(     big.get_int_or_float(12345,   sentinel), 12345)
130 |         self.assertIsInstance(big.get_int_or_float(12345,   sentinel), int)
131 |         self.assertEqual(     big.get_int_or_float("12345", sentinel), 12345)
132 |         self.assertIsInstance(big.get_int_or_float("12345", sentinel), int)
133 | 
134 |         self.assertEqual(     big.get_int_or_float(0.0,     sentinel), 0)
135 |         self.assertIsInstance(big.get_int_or_float("0.0",   sentinel), int)
136 |         self.assertEqual(     big.get_int_or_float(3.5,     sentinel), 3.5)
137 |         self.assertIsInstance(big.get_int_or_float("3.5",   sentinel), float)
138 |         self.assertEqual(     big.get_int_or_float(123.0,   sentinel), 123)
139 |         self.assertIsInstance(big.get_int_or_float("123.0", sentinel), int)
140 | 
141 |         self.assertEqual(big.get_int_or_float("abc",  sentinel), sentinel)
142 |         self.assertEqual(big.get_int_or_float("3j",   sentinel), sentinel)
143 |         self.assertEqual(big.get_int_or_float("None", sentinel), sentinel)
144 |         self.assertEqual(big.get_int_or_float("",     sentinel), sentinel)
145 |         self.assertEqual(big.get_int_or_float(3j,     sentinel), sentinel)
146 |         self.assertEqual(big.get_int_or_float(None,   sentinel), sentinel)
147 |         self.assertEqual(big.get_int_or_float({},     sentinel), sentinel)
148 |         self.assertEqual(big.get_int_or_float((),     sentinel), sentinel)
149 |         self.assertEqual(big.get_int_or_float(set(),  sentinel), sentinel)
150 | 
151 |         d = {}
152 |         self.assertEqual(big.get_int_or_float(d), d)
153 |         self.assertEqual(big.get_int_or_float(3j), 3j)
154 |         self.assertEqual(big.get_int_or_float(None), None)
155 |         self.assertEqual(big.get_int_or_float("abc"), "abc")
156 | 
157 |     def test_pure_virtual(self):
158 |         @big.pure_virtual()
159 |         def uncallable(a): # pragma: no cover
160 |             print(f"hey, look! we wuz called! and a={a}, just ask Ayn Rand!")
161 | 
162 |         with self.assertRaises(NotImplementedError):
163 |             uncallable('a')
164 | 
165 | 
166 | 
167 | def run_tests():
168 |     bigtestlib.run(name="big.builtin", module=__name__)
169 | 
170 | if __name__ == "__main__": # pragma: no cover
171 |     run_tests()
172 |     bigtestlib.finish()
173 | 


--------------------------------------------------------------------------------
/tests/test_deprecated.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | 
  3 | _license = """
  4 | big
  5 | Copyright 2022-2024 Larry Hastings
  6 | All rights reserved.
  7 | 
  8 | Permission is hereby granted, free of charge, to any person obtaining a
  9 | copy of this software and associated documentation files (the "Software"),
 10 | to deal in the Software without restriction, including without limitation
 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
 12 | and/or sell copies of the Software, and to permit persons to whom the
 13 | Software is furnished to do so, subject to the following conditions:
 14 | 
 15 | The above copyright notice and this permission notice shall be included
 16 | in all copies or substantial portions of the Software.
 17 | 
 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 25 | """
 26 | 
 27 | import bigtestlib
 28 | bigtestlib.preload_local_big()
 29 | 
 30 | import big.all as big
 31 | import unittest
 32 | 
 33 | class BigDeprecatedTests(unittest.TestCase):
 34 | 
 35 |     def test_old_separators(self):
 36 |         # now test the deprecated utf-8 variants!
 37 |         # they should match... python str, sigh.
 38 |         # (principle of least surprise.)
 39 |         utf8_whitespace = big.encode_strings(big.str_whitespace, 'utf-8')
 40 |         self.assertEqual(set(big.deprecated.utf8_whitespace), set(utf8_whitespace))
 41 |         utf8_whitespace_without_dos = big.encode_strings(big.str_whitespace_without_crlf, 'utf-8')
 42 |         self.assertEqual(set(big.deprecated.utf8_whitespace_without_dos), set(utf8_whitespace_without_dos))
 43 |         utf8_newlines = big.encode_strings(big.str_linebreaks, 'utf-8')
 44 |         self.assertEqual(set(big.deprecated.utf8_newlines), set(utf8_newlines))
 45 |         utf8_newlines_without_dos = big.encode_strings(big.str_linebreaks_without_crlf, 'utf-8')
 46 |         self.assertEqual(set(big.deprecated.utf8_newlines_without_dos), set(utf8_newlines_without_dos))
 47 | 
 48 |         # test that the compatibility layer for the old "newlines" names is correct
 49 |         self.assertEqual(big.deprecated.newlines, big.str_linebreaks)
 50 |         self.assertEqual(big.deprecated.newlines_without_dos, big.str_linebreaks_without_crlf)
 51 |         self.assertEqual(big.deprecated.ascii_newlines, big.bytes_linebreaks)
 52 |         self.assertEqual(big.deprecated.ascii_newlines_without_dos, big.bytes_linebreaks_without_crlf)
 53 |         self.assertEqual(big.deprecated.utf8_newlines, big.encode_strings(big.linebreaks, 'utf-8'))
 54 |         self.assertEqual(big.deprecated.utf8_newlines_without_dos, big.encode_strings(big.linebreaks_without_crlf, 'utf-8'))
 55 | 
 56 | 
 57 |     def test_split_quoted_strings(self):
 58 |         def test(s, expected, **kwargs):
 59 |             got = list(big.deprecated.split_quoted_strings(s, **kwargs))
 60 |             self.assertEqual(got, expected)
 61 | 
 62 |             got = list(big.deprecated.split_quoted_strings(s.encode('ascii'), **kwargs))
 63 |             self.assertEqual(got, [(b, s.encode('ascii')) for b, s in expected])
 64 | 
 65 |         test("""hey there "this is quoted" an empty quote: '' this is not quoted 'this is more quoted' "here's quoting a quote mark: \\" wow!" this is working!""",
 66 |             [
 67 |                 (False, 'hey there '),
 68 |                 (True, '"this is quoted"'),
 69 |                 (False, ' an empty quote: '),
 70 |                 (True, "''"),
 71 |                 (False, ' this is not quoted '),
 72 |                 (True, "'this is more quoted'"),
 73 |                 (False, ' '),
 74 |                 (True, '"here\'s quoting a quote mark: \\" wow!"'),
 75 |                 (False, ' this is working!'),
 76 |             ])
 77 | 
 78 |         test('''here is triple quoted: """i am triple quoted.""" wow!  again: """triple quoted here. "quotes in quotes" empty: "" done.""" phew!''',
 79 |             [
 80 |                 (False, 'here is triple quoted: '),
 81 |                 (True, '"""i am triple quoted."""'),
 82 |                 (False, ' wow!  again: '),
 83 |                 (True, '"""triple quoted here. "quotes in quotes" empty: "" done."""'),
 84 |                 (False, ' phew!'),
 85 |             ])
 86 | 
 87 |         test('''test turning off quoted strings.  """howdy doodles""" it kinda works anyway!''',
 88 |             [
 89 |                 (False, 'test turning off quoted strings.  '),
 90 |                 (True, '""'),
 91 |                 (True, '"howdy doodles"'),
 92 |                 (True, '""'),
 93 |                 (False, ' it kinda works anyway!'),
 94 |             ],
 95 |             triple_quotes=False)
 96 | 
 97 |     def test_parse_delimiters(self):
 98 | 
 99 |         self.maxDiff = 2**32
100 | 
101 |         def test(s, expected, *, delimiters=None):
102 |             empty = ''
103 |             for i in range(2):
104 |                 got = tuple(big.deprecated.parse_delimiters(s, delimiters=delimiters))
105 | 
106 |                 flattened = []
107 |                 for t in got:
108 |                     flattened.extend(t)
109 |                 s2 = empty.join(flattened)
110 |                 self.assertEqual(s, s2)
111 | 
112 |                 self.assertEqual(expected, got)
113 | 
114 |                 if not i:
115 |                     s = big.encode_strings(s)
116 |                     expected = big.encode_strings(expected)
117 |                     empty = b''
118 | 
119 |         test('a[x] = foo("howdy (folks)\\n", {1:2, 3:4})',
120 |             (
121 |                 ('a',                '[',  ''),
122 |                 ('x',                 '', ']'),
123 |                 (' = foo',           '(',  ''),
124 |                 ('',                 '"',  ''),
125 |                 ('howdy (folks)\\n',  '', '"'),
126 |                 (', ',               '{',  ''),
127 |                 ('1:2, 3:4',          '', '}'),
128 |                 ('',                  '', ')'),
129 |             ),
130 |             )
131 | 
132 |         test('a[[[z]]]{{{{q}}}}[{[{[{[{z}]}]}]}]!',
133 |             (
134 |                 ('a', '[',  ''),
135 |                 ('',  '[',  ''),
136 |                 ('',  '[',  ''),
137 |                 ('z',  '', ']'),
138 |                 ('',   '', ']'),
139 |                 ('',   '', ']'),
140 |                 ('',  '{',  ''),
141 |                 ('',  '{',  ''),
142 |                 ('',  '{',  ''),
143 |                 ('',  '{',  ''),
144 |                 ('q',  '', '}'),
145 |                 ('',   '', '}'),
146 |                 ('',   '', '}'),
147 |                 ('',   '', '}'),
148 |                 ('',  '[',  ''),
149 |                 ('',  '{',  ''),
150 |                 ('',  '[',  ''),
151 |                 ('',  '{',  ''),
152 |                 ('',  '[',  ''),
153 |                 ('',  '{',  ''),
154 |                 ('',  '[',  ''),
155 |                 ('',  '{',  ''),
156 |                 ('z',  '', '}'),
157 |                 ('',   '', ']'),
158 |                 ('',   '', '}'),
159 |                 ('',   '', ']'),
160 |                 ('',   '', '}'),
161 |                 ('',   '', ']'),
162 |                 ('',   '', '}'),
163 |                 ('',   '', ']'),
164 |                 ('!',  '',  ''),
165 |             ),
166 |             )
167 | 
168 |         with self.assertRaises(ValueError):
169 |             test('a[3)', None)
170 |         with self.assertRaises(ValueError):
171 |             test('a{3]', None)
172 |         with self.assertRaises(ValueError):
173 |             test('a(3}', None)
174 | 
175 |         with self.assertRaises(ValueError):
176 |             test('delimiters is empty', None, delimiters=[])
177 |         with self.assertRaises(ValueError):
178 |             test('delimiter is abc (huh!)', None, delimiters=['()', 'abc'])
179 |         with self.assertRaises(TypeError):
180 |             test('delimiters contains 3', None, delimiters=['{}', 3])
181 |         with self.assertRaises(ValueError):
182 |             test('delimiters contains a ', None, delimiters=['<>', '\\/'])
183 |         with self.assertRaises(ValueError):
184 |             test('delimiters contains   ', None, delimiters=['<>', '<>'])
185 | 
186 |         with self.assertRaises(ValueError):
187 |             test('unclosed_paren(a[3]', None)
188 |         with self.assertRaises(ValueError):
189 |             test('x[3] = unclosed_curly{', None)
190 |         with self.assertRaises(ValueError):
191 |             test('foo(a[1], {a[2]: 33}) = unclosed_square[55', None)
192 |         with self.assertRaises(ValueError):
193 |             test('"unterminated string\\', None)
194 |         with self.assertRaises(ValueError):
195 |             test('open_everything( { a[35 "foo', None)
196 | 
197 |         D = big.deprecated.Delimiter
198 |         with self.assertRaises(TypeError):
199 |             D('(', b')')
200 |         with self.assertRaises(TypeError):
201 |             D(b'(', ')')
202 | 
203 | 
204 |     def test_lines_strip_comments(self):
205 |         self.maxDiff = 2**32
206 |         def test(i, expected):
207 |             # print("I", i)
208 |             got = list(i)
209 |             # print("GOT", got)
210 |             # print(f"{i == got=}")
211 |             # print(f"{got == expected=}")
212 |             # import pprint
213 |             # print("\n\n")
214 |             # pprint.pprint(got)
215 |             # print("\n\n")
216 |             # pprint.pprint(expected)
217 |             # print("\n\n")
218 |             self.assertEqual(got, expected)
219 | 
220 |         def L(line, line_number, column_number=1, end='\n', final=None, **kwargs):
221 |             if final is None:
222 |                 final = line
223 |             if isinstance(end, str) and isinstance(line, bytes):
224 |                 end = end.encode('ascii')
225 |             info = big.LineInfo(lines, line + end, line_number, column_number, end=end, **kwargs)
226 |             return (info, final)
227 | 
228 |         lines = big.lines("""
229 | for x in range(5): # this is a comment
230 |     print("# this is quoted", x)
231 |     print("") # this "comment" is useless
232 |     print(no_comments_or_quotes_on_this_line)
233 | """[1:])
234 |         test(big.deprecated.lines_strip_comments(lines, ("#", "//")),
235 |             [
236 |                 L(line='for x in range(5): # this is a comment', line_number=1, column_number=1, trailing=' # this is a comment', final='for x in range(5):'),
237 |                 L(line='    print("# this is quoted", x)', line_number=2, column_number=1),
238 |                 L(line='    print("") # this "comment" is useless', line_number=3, column_number=1, trailing=' # this "comment" is useless', final='    print("")'),
239 |                 L(line='    print(no_comments_or_quotes_on_this_line)', line_number=4, column_number=1),
240 |                 L(line='', line_number=5, column_number=1, end=''),
241 |             ])
242 | 
243 |         # don't get alarmed!  we intentionally break quote characters in this test.
244 |         lines = big.lines("""
245 | for x in range(5): # this is a comment
246 |     print("# this is quoted", x)
247 |     print("") # this "comment" is useless
248 |     print(no_comments_or_quotes_on_this_line)
249 | """[1:])
250 |         test(big.deprecated.lines_strip_comments(lines, ("#", "//"), quotes=None),
251 |             [
252 |                 L(line='for x in range(5): # this is a comment', line_number=1, column_number=1, trailing=' # this is a comment', final='for x in range(5):'),
253 |                 L(line='    print("# this is quoted", x)', line_number=2, column_number=1, trailing='# this is quoted", x)', final='    print("'),
254 |                 L(line='    print("") # this "comment" is useless', line_number=3, column_number=1, trailing=' # this "comment" is useless', final='    print("")'),
255 |                 L(line='    print(no_comments_or_quotes_on_this_line)', line_number=4, column_number=1),
256 |                 L(line='', line_number=5, column_number=1, end=''),
257 |             ])
258 | 
259 |         with self.assertRaises(ValueError):
260 |             test(big.deprecated.lines_strip_comments(big.lines("a\nb\n"), None), [])
261 | 
262 |         lines = big.lines(b"a\nb# ignored\n c")
263 |         test(big.deprecated.lines_strip_comments(lines, b'#'),
264 |             [
265 |             L(b'a', 1, ),
266 |             L(b'b# ignored', 2, 1, trailing=b'# ignored', final=b'b'),
267 |             L(b' c', 3, end=b''),
268 |             ]
269 |             )
270 | 
271 | 
272 | 
273 | def run_tests():
274 |     bigtestlib.run(name="big.deprecated", module=__name__)
275 | 
276 | if __name__ == "__main__": # pragma: no cover
277 |     run_tests()
278 |     bigtestlib.finish()
279 | 


--------------------------------------------------------------------------------
/tests/test_encodings/ascii_source_code_encoding.py:
--------------------------------------------------------------------------------
1 | # -*- coding: ascii -*-#
2 | 
3 | print("Hello, D.C.!")
4 | substitute_squirrel = "Squirrel &o"
5 | 


--------------------------------------------------------------------------------
/tests/test_encodings/gb18030_bom.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larryhastings/big/d91ffb12841e5970cf07262e9d7ecbe37bdc8b91/tests/test_encodings/gb18030_bom.py


--------------------------------------------------------------------------------
/tests/test_encodings/invalid_conflicting_bom_and_source_code_encoding.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larryhastings/big/d91ffb12841e5970cf07262e9d7ecbe37bdc8b91/tests/test_encodings/invalid_conflicting_bom_and_source_code_encoding.py


--------------------------------------------------------------------------------
/tests/test_encodings/invalid_source_code_encoding.py:
--------------------------------------------------------------------------------
1 | # -*- coding: kwisatz haderach -*-
2 | print("Hello, Dune!")
3 | 


--------------------------------------------------------------------------------
/tests/test_encodings/invalid_utf-1_bom.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larryhastings/big/d91ffb12841e5970cf07262e9d7ecbe37bdc8b91/tests/test_encodings/invalid_utf-1_bom.py


--------------------------------------------------------------------------------
/tests/test_encodings/utf-7_bom.py:
--------------------------------------------------------------------------------
1 | +/v# This file is encoded using "utf-7",
2 | # and starts with the appropriate BOM.
3 | 
4 | print('Hello, Mountain View!')
5 | print('Chipmunk +2D3cP/4P!')
6 | 


--------------------------------------------------------------------------------
/tests/test_encodings/utf-7_source_code_encoding.py:
--------------------------------------------------------------------------------
1 | # coding=utf-7
2 | # This file is encoded using "utf-7",
3 | # and starts with the appropriate source code encoding.
4 | 
5 | print('Hello, Mountain View!')
6 | print('Chipmunk +2D3cP/4P!')
7 | 


--------------------------------------------------------------------------------
/tests/test_encodings/utf-8_source_code_encoding.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | 
 3 | # This file serves a dual purpose in the test suite!
 4 | # It both uses an ASCII "source code encoding" line,
 5 | # which lets us test decoding those lines.
 6 | #
 7 | # Also, when run it generates two more Python scripts,
 8 | # using unfamiliar encodings, both with BOMs:
 9 | #     * "gb18030_bom.py" uses the "gb18030" encoding.
10 | #       This is an encoding defined by the PRC.
11 | #     * "utf-1_bom.py" uses the "UTF-1" encoding,
12 | #       an obsolete encoding replaced by the far
13 | #       superior UTF-8.
14 | #
15 | # Note that Python doesn't support UTF-1!
16 | # This is useful to test the "we don't support this encoding" logic.
17 | # 100% coverage without pragma nocover, here we come!
18 | 
19 | script = """
20 | # This file is encoded using "{encoding}",
21 | # and starts with the appropriate BOM.
22 | 
23 | print('Hello, {city}!')
24 | print('Chipmunk 🐿️!')
25 | """.strip() + "\n"
26 | 
27 | for (encoding, use_encoding, bom, city, valid) in (
28 |     ('gb18030', 'gb18030', b"\x84\x31\x95\x33", "Beijing", True),
29 |     ('utf-1', 'utf-8', b"\xF7\x64\x4C", "Mountain View", False),
30 |     ('utf-7', 'utf-7', b"\x2b\x2f\x76", "Mountain View", True),
31 |     ):
32 | 
33 |     bytes = script.format(encoding=encoding, city=city).encode(use_encoding)
34 |     invalid_prefix = "" if valid else "invalid_"
35 |     filename = f"{invalid_prefix}{encoding}_bom.py"
36 | 
37 |     with open(filename, "wb") as f:
38 |         f.write(bom)
39 |         f.write(bytes)
40 | 


--------------------------------------------------------------------------------
/tests/test_encodings/utf16_bom.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larryhastings/big/d91ffb12841e5970cf07262e9d7ecbe37bdc8b91/tests/test_encodings/utf16_bom.py


--------------------------------------------------------------------------------
/tests/test_file.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | 
  3 | _license = """
  4 | big
  5 | Copyright 2022-2024 Larry Hastings
  6 | All rights reserved.
  7 | 
  8 | Permission is hereby granted, free of charge, to any person obtaining a
  9 | copy of this software and associated documentation files (the "Software"),
 10 | to deal in the Software without restriction, including without limitation
 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
 12 | and/or sell copies of the Software, and to permit persons to whom the
 13 | Software is furnished to do so, subject to the following conditions:
 14 | 
 15 | The above copyright notice and this permission notice shall be included
 16 | in all copies or substantial portions of the Software.
 17 | 
 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 25 | """
 26 | 
 27 | import bigtestlib
 28 | big_dir = bigtestlib.preload_local_big()
 29 | 
 30 | import big.all as big
 31 | import glob
 32 | import os.path
 33 | from pathlib import Path
 34 | import re
 35 | import shutil
 36 | import tempfile
 37 | import time
 38 | import unittest
 39 | 
 40 | 
 41 | def unchanged(o):
 42 |     return o
 43 | 
 44 | def to_bytes(o):
 45 |     if isinstance(o, str):
 46 |         return o.encode('ascii')
 47 |     if isinstance(o, list):
 48 |         return [to_bytes(x) for x in o]
 49 |     if isinstance(o, tuple):
 50 |         return tuple(to_bytes(x) for x in o)
 51 |     return o
 52 | 
 53 | class BigFileTests(unittest.TestCase):
 54 | 
 55 |     def test_grep(self):
 56 |         test_dir = os.path.dirname(__file__)
 57 |         grepfile = os.path.join(test_dir, "grepfile")
 58 |         for c in (unchanged, to_bytes):
 59 |             self.assertEqual(big.grep(c(grepfile), c("b")), c(['bbbb', 'abc']))
 60 |             self.assertEqual(big.grep(c(grepfile), c("b"), enumerate=True), c([(2, 'bbbb'), (3, 'abc')]))
 61 | 
 62 |             self.assertEqual(big.grep(c(grepfile), c("[bc]")), c(['bbbb', 'abc', 'cccc']))
 63 |             self.assertEqual(big.grep(c(grepfile), re.compile(c("[bc]"))), c(['bbbb', 'abc', 'cccc']))
 64 | 
 65 |             self.assertEqual(big.grep(c(grepfile), c("b"), flags=re.I), c(['bbbb', 'abc', 'BBBB', 'ABC']))
 66 |             self.assertEqual(big.grep(c(grepfile), c("B"), flags=re.I), c(['bbbb', 'abc', 'BBBB', 'ABC']))
 67 |             self.assertEqual(big.grep(c(grepfile), c("b"), flags=re.I, enumerate=True), c([(2, 'bbbb'), (3, 'abc'), (7, 'BBBB'), (8, 'ABC')]))
 68 | 
 69 |             p = Path(grepfile)
 70 |             self.assertEqual(big.grep(p, c("b")), c(['bbbb', 'abc']))
 71 | 
 72 |         with self.assertRaises(ValueError):
 73 |             self.assertEqual(big.grep(p, b"b", encoding="utf-8"))
 74 | 
 75 |     def test_fgrep(self):
 76 |         test_dir = os.path.dirname(__file__)
 77 |         grepfile = os.path.join(test_dir, "grepfile")
 78 |         for c in (unchanged, to_bytes):
 79 |             self.assertEqual(big.fgrep(c(grepfile), c("b")), c(['bbbb', 'abc']))
 80 |             self.assertEqual(big.fgrep(c(grepfile), c("b"), case_insensitive=True), c(['bbbb', 'abc', 'BBBB', 'ABC']))
 81 |             self.assertEqual(big.fgrep(c(grepfile), c("B"), case_insensitive=True), c(['bbbb', 'abc', 'BBBB', 'ABC']))
 82 |             self.assertEqual(big.fgrep(c(grepfile), c("b"), enumerate=True), c([(2, 'bbbb'), (3, 'abc')]))
 83 |             self.assertEqual(big.fgrep(c(grepfile), c("b"), case_insensitive=True, enumerate=True), c([(2, 'bbbb'), (3, 'abc'), (7, 'BBBB'), (8, 'ABC')]))
 84 |             p = Path(grepfile)
 85 |             self.assertEqual(big.fgrep(p, c("b")), c(['bbbb', 'abc']))
 86 | 
 87 |         with self.assertRaises(ValueError):
 88 |             self.assertEqual(big.fgrep(p, b"b", encoding="utf-8"))
 89 | 
 90 |     def test_pushd(self):
 91 |         cwd = os.getcwd()
 92 |         with big.pushd(".."):
 93 |             self.assertEqual(os.getcwd(), os.path.dirname(cwd))
 94 |         self.assertEqual(os.getcwd(), cwd)
 95 | 
 96 | class BigFileTmpdirTests(unittest.TestCase):
 97 | 
 98 |     def setUp(self):
 99 |         self.tmpdir = tempfile.mkdtemp(prefix="bigtest")
100 | 
101 |     def tearDown(self):
102 |         shutil.rmtree(self.tmpdir)
103 | 
104 |     def test_safe_mkdir(self):
105 |         # newdir points to a directory that doesn't exist yet
106 |         newdir = os.path.join(self.tmpdir, "newdir")
107 |         self.assertFalse(os.path.isdir(newdir))
108 | 
109 |         # mkdir and check that it worked
110 |         big.safe_mkdir(newdir)
111 |         self.assertTrue(os.path.isdir(newdir))
112 | 
113 |         # doing it a second time does nothing
114 |         big.safe_mkdir(newdir)
115 |         self.assertTrue(os.path.isdir(newdir))
116 | 
117 |         # test unlinking a file
118 |         y = os.path.join(self.tmpdir, "y")
119 |         with open(y, "wt") as f:
120 |             f.write("x")
121 |         big.safe_mkdir(y)
122 |         self.assertTrue(os.path.isdir(y))
123 | 
124 |         # make a file named 'x', then ask to
125 |         # safe_mkdir 'x/y'
126 |         newfile = os.path.join(self.tmpdir, "newfile")
127 |         with open(newfile, "wt") as f:
128 |             f.write("x")
129 |         newsubfile = os.path.join(newfile, 'y')
130 |         with self.assertRaises(NotADirectoryError):
131 |             big.safe_mkdir(newsubfile)
132 | 
133 |     def test_safe_unlink(self):
134 |         newfile = os.path.join(self.tmpdir, "newfile")
135 |         self.assertFalse(os.path.isfile(newfile))
136 |         big.safe_unlink(newfile)
137 |         with open(newfile, "wt") as f:
138 |             f.write("x")
139 |         self.assertTrue(os.path.isfile(newfile))
140 |         big.safe_unlink(newfile)
141 |         self.assertFalse(os.path.isfile(newfile))
142 |         big.safe_unlink(newfile)
143 |         self.assertFalse(os.path.isfile(newfile))
144 | 
145 | 
146 |     def test_file_size(self):
147 |         newfile = os.path.join(self.tmpdir, "newfile")
148 |         with self.assertRaises(FileNotFoundError):
149 |             big.file_size(newfile)
150 |         with open(newfile, "wt") as f:
151 |             f.write("abcdefgh")
152 |         self.assertEqual(big.file_size(newfile), 8)
153 | 
154 |     def test_file_mtime(self):
155 |         newfile = os.path.join(self.tmpdir, "newfile")
156 |         with self.assertRaises(FileNotFoundError):
157 |             big.file_mtime(newfile)
158 |         with open(newfile, "wt") as f:
159 |             f.write("abcdefgh")
160 |         self.assertGreater(big.file_mtime(newfile), big.file_mtime(__file__))
161 | 
162 |     def test_file_mtime_ns(self):
163 |         newfile = os.path.join(self.tmpdir, "newfile")
164 |         with self.assertRaises(FileNotFoundError):
165 |             big.file_mtime_ns(newfile)
166 |         with open(newfile, "wt") as f:
167 |             f.write("abcdefgh")
168 |         self.assertGreater(big.file_mtime_ns(newfile), big.file_mtime_ns(__file__))
169 | 
170 |     def test_touch(self):
171 |         firstfile = os.path.join(self.tmpdir, "firstfile")
172 |         with open(firstfile, "wt") as f:
173 |             f.write("abcdefgh")
174 |         st = os.stat(firstfile)
175 |         newfile = os.path.join(self.tmpdir, "newfile")
176 |         self.assertFalse(os.path.isfile(newfile))
177 |         big.touch(newfile)
178 |         self.assertTrue(os.path.isfile(newfile))
179 |         self.assertGreater(big.file_mtime_ns(newfile), big.file_mtime_ns(__file__))
180 |         newfile2 = newfile + "2"
181 |         big.touch(newfile2)
182 |         time_in_the_past = big.file_mtime_ns(newfile) - 2**32
183 |         os.utime(newfile2, ns=(time_in_the_past, time_in_the_past))
184 |         self.assertGreaterEqual(big.file_mtime_ns(newfile), big.file_mtime_ns(newfile2))
185 |         time.sleep(0.01)
186 |         big.touch(newfile2)
187 |         self.assertGreater(big.file_mtime_ns(newfile2), big.file_mtime_ns(newfile))
188 | 
189 |     def test_translate_filename_to_exfat(self):
190 |         self.assertEqual(big.translate_filename_to_exfat("abcde"), "abcde")
191 |         before = 'abc\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f/\\:*?": <>|xyz'
192 |         after = "abc@?01?02?03?04?05?06?07?08?09?0a?0b?0c?0d?0e?0f?10?11?12?13?14?15?16?17?18?19?1a?1b?1c?1d?1e?1f--.@.' - {}!xyz"
193 |         self.assertEqual(big.translate_filename_to_exfat(before), after)
194 | 
195 |         with self.assertRaises(ValueError):
196 |             big.translate_filename_to_exfat(35)
197 | 
198 |     def test_translate_filename_to_unix(self):
199 |         self.assertEqual(big.translate_filename_to_unix("abcde"), "abcde")
200 |         self.assertEqual(big.translate_filename_to_unix('ab\x00de/fg'), 'ab@de-fg')
201 | 
202 |         with self.assertRaises(ValueError):
203 |             big.translate_filename_to_unix(35)
204 | 
205 |     def test_read_python_file(self):
206 |         """
207 |         I don't need to write any read_python_file tests here.
208 |         It's given a thorough workout elsewhere in the unit test suite:
209 |                 file tests/test_text.py
210 |             function test_python_delimiters_on_big_source_tree()
211 | 
212 |         Also, it's a thin wrapper around decode_python_script,
213 |         which also gets a nice workout:
214 |                 file tests/test_text.py
215 |             function test_decode_python_script()
216 |         """
217 |         pass
218 | 
219 |     def test_search_path(self):
220 |         # ensure that glob.escape and normcase are composable in either order
221 |         # (they should be)
222 |         self.assertEqual(
223 |             os.path.normcase( glob.escape("AbC*") ),
224 |             glob.escape( os.path.normcase("AbC*") ),
225 |             )
226 | 
227 |         with big.pushd(big_dir / "tests"):
228 | 
229 |             foo_d   = Path("test_search_path/foo_d_path/foo.d")
230 |             foo_h   = Path("test_search_path/foo_h_path/foo.h")
231 |             foo_hpp = Path("test_search_path/foo_h_path/foo.hpp")
232 |             foobar  = Path("test_search_path/file_without_extension/foobar")
233 |             mydir   = Path("test_search_path/want_directories/mydir")
234 | 
235 |             search = big.search_path(
236 |                 ["test_search_path/foo_h_path", "test_search_path/foo_d_path"],
237 |                 ('.D',),
238 |                 preserve_extension=False,
239 |                 case_sensitive=False,
240 |                 want_directories=True,
241 |                 )
242 |             self.assertEqual(search('nonexists'), None)
243 |             self.assertEqual(search('foo'), foo_d)
244 |             self.assertEqual(search('foo.d'), None)
245 | 
246 |             search = big.search_path(
247 |                 ["test_search_path/foo_h_path", "test_search_path/foo_d_path"],
248 |                 ('', '.D'),
249 |                 preserve_extension=True,
250 |                 case_sensitive=False,
251 |                 want_directories=True,
252 |                 )
253 |             self.assertEqual(search('foo'), foo_d)
254 |             self.assertEqual(search('foo.d'), foo_d)
255 | 
256 |             search = big.search_path(
257 |                 ["test_search_path/foo_h_path", "test_search_path/foo_d_path"],
258 |                 ('.D',),
259 |                 preserve_extension=True,
260 |                 case_sensitive=False,
261 |                 want_directories=True,
262 |                 )
263 |             self.assertEqual(search('foo'), foo_d)
264 |             self.assertEqual(search('foo.d'), foo_d)
265 | 
266 |             search = big.search_path(
267 |                 ["test_search_path/foo_h_path", "test_search_path/foo_d_path"],
268 |                 ('.D',),
269 |                 preserve_extension=True,
270 |                 case_sensitive=False,
271 |                 want_directories=False,
272 |                 )
273 |             self.assertEqual(search('foo'), None)
274 |             self.assertEqual(search('foo.d'), None)
275 | 
276 |             search = big.search_path(
277 |                 ["test_search_path/foo_d_path", "test_search_path/foo_h_path"],
278 |                 ('.H',),
279 |                 preserve_extension=True,
280 |                 case_sensitive=False,
281 |                 want_directories=False,
282 |                 )
283 |             self.assertEqual(search('foo'), foo_h)
284 |             self.assertEqual(search('foo.h'), foo_h)
285 | 
286 |             foo_h = Path("test_search_path/foo_h_path/foo.h")
287 |             search = big.search_path(
288 |                 ["test_search_path/foo_d_path", "test_search_path/foo_h_path"],
289 |                 ('.H',),
290 |                 preserve_extension=False,
291 |                 case_sensitive=False,
292 |                 want_directories=False,
293 |                 )
294 |             self.assertEqual(search('foo'), foo_h)
295 |             self.assertEqual(search('foo.h'), None)
296 | 
297 |             search = big.search_path(
298 |                 ["test_search_path/foo_d_path", "test_search_path/foo_h_path"],
299 |                 ('.H',),
300 |                 preserve_extension=True,
301 |                 case_sensitive=True,
302 |                 want_directories=False,
303 |                 )
304 |             self.assertEqual(search('foo'), None)
305 |             self.assertEqual(search('foo.h'), None)
306 | 
307 |             search = big.search_path(
308 |                 ["test_search_path/foo_d_path", "test_search_path/foo_h_path"],
309 |                 ('.h',),
310 |                 preserve_extension=True,
311 |                 want_directories=False,
312 |                 )
313 |             self.assertEqual(search('foo'), foo_h)
314 |             self.assertEqual(search('foo.h'), foo_h)
315 | 
316 |             search = big.search_path(
317 |                 ["nonexistent_dir", "test_search_path/foo_d_path", "test_search_path/foo_h_path"],
318 |                 ('.h',),
319 |                 preserve_extension=True,
320 |                 want_directories=True,
321 |                 want_files=False,
322 |                 )
323 |             self.assertEqual(search('foo'), None)
324 |             self.assertEqual(search('foo.h'), None)
325 | 
326 |             search = big.search_path(
327 |                 ["test_search_path/this_file_doesnt_match_anything", "test_search_path/foo_d_path", "test_search_path/foo_h_path"],
328 |                 ('.h', '.hpp'),
329 |                 preserve_extension=True,
330 |                 )
331 |             self.assertEqual(search('foo'), foo_h)
332 |             self.assertEqual(search('foo.h'), foo_h)
333 |             self.assertEqual(search('foo.hpp'), foo_hpp)
334 | 
335 |             search = big.search_path(
336 |                 ["test_search_path/foo_d_path", "test_search_path/foo_h_path", "test_search_path/file_without_extension"],
337 |                 ('.x', '.xyz', '',),
338 |                 preserve_extension=True,
339 |                 )
340 |             self.assertEqual(search('foobar'), foobar)
341 |             self.assertEqual(search('foo.h'), foo_h)
342 |             self.assertEqual(search('foo.hpp'), foo_hpp)
343 | 
344 |             search = big.search_path(
345 |                 ["test_search_path/foo_d_path", "test_search_path/want_directories"],
346 |                 ('.x', '.xyz', '',),
347 |                 preserve_extension=True,
348 |                 want_directories=True,
349 |                 want_files=False
350 |                 )
351 |             self.assertEqual(search('mydir'), mydir)
352 |             self.assertEqual(search('yourdir'), None)
353 | 
354 |             with self.assertRaises(ValueError):
355 |                 big.search_path(("a", "b", "c"), want_files=False, want_directories=False)
356 |             with self.assertRaises(ValueError):
357 |                 big.search_path([])
358 |             with self.assertRaises(ValueError):
359 |                 big.search_path(("a", "b", "c"), [])
360 |             with self.assertRaises(ValueError):
361 |                 big.search_path(("a", "b", "c"), ('.a', 33))
362 |             with self.assertRaises(ValueError):
363 |                 big.search_path(("a", "b", "c"), ('.a', 'bcd'))
364 |             with self.assertRaises(ValueError):
365 |                 big.search_path(("a", "b", "c"), ('.a', '', '.bcd', ''))
366 | 
367 |             with self.assertRaises(ValueError):
368 |                 search("foobar/")
369 | 
370 |             with tempfile.TemporaryDirectory() as tmp:
371 |                 tmp = Path(tmp)
372 |                 lower = tmp / "filename.h"
373 |                 upper = tmp / "FILENAME.h"
374 |                 big.touch(lower)
375 |                 if not upper.exists():
376 |                     big.touch(upper)
377 | 
378 |                     search = big.search_path([tmp], ['.h', ''], case_sensitive=False)
379 |                     with self.assertRaises(ValueError):
380 |                         search("fIlEnAmE")
381 |                     search = big.search_path([tmp], case_sensitive=False)
382 |                     with self.assertRaises(ValueError):
383 |                         search("fIlEnAmE.h")
384 | 
385 | def run_tests():
386 |     bigtestlib.run(name="big.file", module=__name__)
387 | 
388 | if __name__ == "__main__": # pragma: no cover
389 |     run_tests()
390 |     bigtestlib.finish()
391 | 


--------------------------------------------------------------------------------
/tests/test_graph.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | 
  3 | _license = """
  4 | big
  5 | Copyright 2022-2024 Larry Hastings
  6 | All rights reserved.
  7 | 
  8 | Permission is hereby granted, free of charge, to any person obtaining a
  9 | copy of this software and associated documentation files (the "Software"),
 10 | to deal in the Software without restriction, including without limitation
 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
 12 | and/or sell copies of the Software, and to permit persons to whom the
 13 | Software is furnished to do so, subject to the following conditions:
 14 | 
 15 | The above copyright notice and this permission notice shall be included
 16 | in all copies or substantial portions of the Software.
 17 | 
 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 25 | """
 26 | 
 27 | import bigtestlib
 28 | bigtestlib.preload_local_big()
 29 | 
 30 | from big.all import TopologicalSorter
 31 | import itertools
 32 | import unittest
 33 | 
 34 | 
 35 | def _parse(nodes_and_dependencies):
 36 |     args = []
 37 |     for line in nodes_and_dependencies.strip().split("\n"):
 38 |         line = line.strip()
 39 |         if (not line) or line.startswith("#"):
 40 |             continue
 41 |         a = line.split()
 42 |         args.append(a)
 43 |     return args
 44 | 
 45 | def parse(nodes_and_dependencies):
 46 |     args = _parse(nodes_and_dependencies)
 47 |     graph = TopologicalSorter()
 48 |     for a in args:
 49 |         graph.add(*a)
 50 |     return graph
 51 | 
 52 | tests_run = 0
 53 | 
 54 | class TopologicalSortTests(unittest.TestCase):
 55 | 
 56 |     def permute_tests(self, nodes_and_dependencies, result, *, remove="", cycle=None):
 57 |         global tests_run
 58 | 
 59 |         args = _parse(nodes_and_dependencies)
 60 | 
 61 |         # we try every ordering of adding the nodes
 62 |         # and, if there are removals, for each of those
 63 |         #    we try every ordering of the removals
 64 |         for args in itertools.permutations(args):
 65 |             if remove:
 66 |                 remove_iterator = itertools.permutations(remove)
 67 |             else:
 68 |                 remove_iterator = (None,)
 69 |             for removals in remove_iterator:
 70 |                 graph = TopologicalSorter()
 71 |                 for a in args:
 72 |                     graph.add(*a)
 73 |                 if removals:
 74 |                     for s in removals:
 75 |                         graph.remove(s)
 76 | 
 77 |                 if cycle:
 78 |                     c = graph.cycle()
 79 |                     self.assertSetEqual(set(graph.cycle()), set(cycle), msg=f"{graph.cycle()} != {cycle}")
 80 |                     return
 81 | 
 82 |                 self.assertFalse(graph.cycle())
 83 |                 yielded = []
 84 |                 while graph:
 85 |                     ready = graph.ready()
 86 |                     self.assertTrue(ready)
 87 |                     yielded.extend(ready)
 88 |                     graph.done(*ready)
 89 |                 got = "".join(sorted(yielded))
 90 |                 self.assertEqual(got, result, msg=f"expected result={result} is not equal to got={got!r}")
 91 |                 tests_run += 1
 92 | 
 93 | 
 94 |     def test_simple_cycle(self):
 95 |         self.permute_tests("A A", cycle="A", result="A")
 96 | 
 97 |     nodes_and_dependencies = """
 98 |         A
 99 |         B   A
100 |         C   A
101 |         D   B C
102 |         X   A B C D
103 |         E   C D X
104 |         Y   C D X E
105 |         """
106 | 
107 |     def test_basic_graph(self):
108 |         self.permute_tests(self.nodes_and_dependencies, result="ABCDEXY")
109 | 
110 |     def test_with_removal(self):
111 |         self.permute_tests(self.nodes_and_dependencies, remove="XY", result="ABCDE")
112 | 
113 |     nodes_and_dependencies_with_cycle = nodes_and_dependencies + """
114 |         F   B
115 |         A   F
116 |         """
117 |     def test_complex_cycle(self):
118 |         self.permute_tests(self.nodes_and_dependencies_with_cycle, cycle="ABF", result="ABCDE")
119 | 
120 |     def test_complex_cycle_with_removal(self):
121 |         self.permute_tests(self.nodes_and_dependencies_with_cycle, remove="CDX", cycle="ABF", result="ABCDE")
122 | 
123 |     def test_removals(self):
124 |         self.permute_tests("""
125 |             A
126 |             B   A
127 |             C   B A
128 |             D   C B A
129 |             E   D C B A
130 |             """, remove="DBC", result="AE")
131 | 
132 |     def test_incoherence(self):
133 |         # test view incoherence:
134 |         # if you add an edge A-1, where 1 depends on A,
135 |         # the view is coherent only if one of these statements is true:
136 |         #   * 1 has not been yielded, or
137 |         #   * A has been marked as done.
138 |         global tests_run
139 | 
140 |         for predecessor_state in range(3):
141 |             for successor_state in range(3):
142 |                 for delete_predecessor in (False, True):
143 |                     g = TopologicalSorter()
144 | 
145 |                     if predecessor_state != 0:
146 |                         g.add("A")
147 |                     if successor_state != 0:
148 |                         g.add("1")
149 | 
150 |                     v = g.view()
151 |                     ready = v.ready()
152 |                     if predecessor_state != 0:
153 |                         self.assertIn('A', ready)
154 |                     if successor_state != 0:
155 |                         self.assertIn('1', ready)
156 | 
157 |                     if predecessor_state == 2:
158 |                         v.done("A")
159 |                     if successor_state == 2:
160 |                         v.done("1")
161 | 
162 |                     if predecessor_state == 0:
163 |                         g.add("A")
164 |                     if successor_state == 0:
165 |                         g.add("1")
166 | 
167 |                     g.add('1', "A")
168 | 
169 |                     should_be_coherent = (
170 |                         (predecessor_state == 2)
171 |                         or
172 |                         (successor_state == 0)
173 |                         )
174 | 
175 |                     try:
176 |                         bool(v)
177 |                         coherent = True
178 |                     except RuntimeError:
179 |                         coherent = False
180 | 
181 |                     # print(f"predecessor_state={predecessor_state} successor_state={successor_state} should_be_coherent={should_be_coherent} coherent={coherent}")
182 |                     # g._default_view.print()
183 |                     # print()
184 | 
185 |                     self.assertEqual(should_be_coherent, coherent, msg=f"test 1 predecessor_state={predecessor_state} successor_state={successor_state} should_be_coherent={should_be_coherent} coherent={coherent}")
186 |                     tests_run += 1
187 | 
188 |                     if coherent:
189 |                         continue
190 | 
191 |                     # now delete one of the two nodes and assert that the graph is returned to coherence
192 |                     g.remove("A" if delete_predecessor else '1')
193 |                     bool(v)
194 |                     v.close()
195 |                     tests_run += 1
196 | 
197 |     def generate_groups(self):
198 |         # first, build a list of the groups we get from an iterator
199 |         g = parse(self.nodes_and_dependencies)
200 |         g_groups = []
201 |         while g:
202 |             r = g.ready()
203 |             g_groups.append(r)
204 |             g.done(*r)
205 |         return g, g_groups
206 | 
207 |     def test_reset(self):
208 |         global tests_run
209 |         g, g_groups = self.generate_groups()
210 |         # test reset()
211 |         g.reset()
212 |         for i, r in enumerate(g_groups, 1):
213 |             r2 = g.ready()
214 |             self.assertEqual(r, r2, msg=f"failed at step {i}: r={r!r} != r2={r2}")
215 |             g.done(*r2)
216 |             tests_run += 1
217 | 
218 |     def test_mutation(self):
219 |         # test mutating the graph while iterating over it.
220 |         # we add unrelated nodes at each step while walking the graph
221 |         # and see that they're returned in proper order.
222 |         global tests_run
223 |         g, g_groups = self.generate_groups()
224 | 
225 |         # adding this empty tuple lets the test work even after we run out
226 |         # of the original nodes we added (when we add 1 and 2 right at the end).
227 |         g_groups.append(())
228 | 
229 |         for step in range(len(g_groups)):
230 |             g = parse(self.nodes_and_dependencies)
231 |             i = 0
232 |             while g:
233 |                 if i == step:
234 |                     g.add("2", "1")
235 |                 r = g.ready()
236 |                 self.assertLessEqual(set(g_groups[i]), set(r), f"set(g_groups[i])={set(g_groups[i])} isn't <= set(r){set(r)}")
237 |                 if i == step:
238 |                     self.assertIn('1', r, msg=f"1 not in r={r}")
239 |                 elif i == (step + 1):
240 |                     self.assertIn('2', r, msg=f"2 not in r={r}")
241 |                 g.done(*r)
242 |                 i += 1
243 |             tests_run += 1
244 | 
245 |     def test_copy(self):
246 |         g, g_groups = self.generate_groups()
247 |         g2 = g.copy()
248 |         order1 = g.static_order()
249 |         order2 = g2.static_order()
250 |         self.assertEqual(list(order1), list(order2))
251 | 
252 |     def test_len(self):
253 |         g, g_groups = self.generate_groups()
254 |         self.assertEqual(len(g), 7)
255 | 
256 |     def test_empty_graph_is_false(self):
257 |         g = TopologicalSorter()
258 |         self.assertFalse(g)
259 | 
260 |     def test_empty_graph_is_empty(self):
261 |         g = TopologicalSorter()
262 |         self.assertEqual(list(g.static_order()), [])
263 | 
264 |     def test_copying_incoherent_view(self):
265 |         g = TopologicalSorter()
266 |         g.add('B', 'A')
267 |         g.add('C', 'A')
268 |         g.add('D', 'B', 'C')
269 |         ready = g.ready()
270 |         self.assertEqual(ready, ('A',))
271 |         g.add('A', 'D')
272 |         with self.assertRaises(RuntimeError):
273 |             bool(g)
274 |         g2 = g.copy()
275 |         with self.assertRaises(RuntimeError):
276 |             bool(g2)
277 |         with self.assertRaises(RuntimeError):
278 |             g2.ready()
279 |         with self.assertRaises(RuntimeError):
280 |             g2.done('A')
281 | 
282 |     def test_cycle(self):
283 |         g = TopologicalSorter()
284 |         g.add('B', 'A')
285 |         g.add('C', 'A')
286 |         g.add('D', 'B', 'C')
287 |         self.assertEqual(g.cycle(), None)
288 |         # coverage test, we return when the dirty bit isn't set
289 |         self.assertEqual(g.cycle(), None)
290 | 
291 |     def test_remove(self):
292 |         g = TopologicalSorter()
293 |         g.add('B', 'A')
294 |         g.add('C', 'A')
295 |         g.add('D', 'B', 'C')
296 |         self.assertEqual(list(g.static_order()), ['A', 'B', 'C', 'D'])
297 |         g.remove('D')
298 |         self.assertEqual(list(g.static_order()), ['A', 'B', 'C'])
299 |         with self.assertRaises(ValueError):
300 |             g.remove('Q')
301 | 
302 | 
303 |     def test_close(self):
304 |         g = TopologicalSorter()
305 |         v = g.view()
306 |         v.close()
307 |         with self.assertRaises(ValueError):
308 |             bool(v)
309 |         with self.assertRaises(ValueError):
310 |             bool(v.ready())
311 |         with self.assertRaises(ValueError):
312 |             bool(v.done('A'))
313 |         with self.assertRaises(ValueError):
314 |             bool(v.reset())
315 |         with self.assertRaises(ValueError):
316 |             bool(v.close())
317 | 
318 |     def test_print(self):
319 |         output = ""
320 |         def print(*a, end="\n", sep=" "):
321 |             nonlocal output
322 |             output += sep.join(str(o) for o in a) + end
323 | 
324 |         g, g_groups = self.generate_groups()
325 |         v = g.view()
326 |         v.print(print=print)
327 |         self.assertIn('nodes', output)
328 |         self.assertIn('ready', output)
329 |         self.assertIn('yielded', output)
330 |         self.assertIn('done', output)
331 |         self.assertIn('conflict', output)
332 | 
333 |     def test_manually_constructed_graph(self):
334 |         # this isn't a supported API
335 |         # but this test will make coverage happy.
336 |         g = TopologicalSorter()
337 |         v = TopologicalSorter.View(g)
338 |         self.assertFalse(v)
339 |         with self.assertRaises(ValueError):
340 |             v2 = TopologicalSorter.View({})
341 | 
342 | 
343 | 
344 | 
345 | #
346 | # Reusing graphlib tests
347 | #
348 | import big.graph
349 | graphlib = big.graph
350 | 
351 | import sys
352 | sys.modules['graphlib'] = graphlib
353 | 
354 | try:
355 |     import test
356 |     from test import test_graphlib
357 |     # the API has changed and they are now invalid.
358 |     for fn_name in """
359 |         test_calls_before_prepare
360 |         test_prepare_multiple_times
361 |         """.strip().split():
362 |         delattr(test.test_graphlib.TestTopologicalSort, fn_name)
363 |     have_test_graphlib = True
364 | except ImportError: # pragma: no cover
365 |     have_test_graphlib = False
366 | 
367 | 
368 | def run_tests(exit=False):
369 |     bigtestlib.run(name="big.graph", module=__name__, permutations=lambda: tests_run)
370 |     if have_test_graphlib:
371 |         print("Testing big.graph using test.test_graphlib...")
372 |         bigtestlib.run(name=None, module="test.test_graphlib")
373 | 
374 | 
375 | if __name__ == "__main__": # pragma: no cover
376 |     run_tests()
377 |     bigtestlib.finish()
378 | 


--------------------------------------------------------------------------------
/tests/test_heap.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | 
  3 | _license = """
  4 | big
  5 | Copyright 2022-2024 Larry Hastings
  6 | All rights reserved.
  7 | 
  8 | Permission is hereby granted, free of charge, to any person obtaining a
  9 | copy of this software and associated documentation files (the "Software"),
 10 | to deal in the Software without restriction, including without limitation
 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
 12 | and/or sell copies of the Software, and to permit persons to whom the
 13 | Software is furnished to do so, subject to the following conditions:
 14 | 
 15 | The above copyright notice and this permission notice shall be included
 16 | in all copies or substantial portions of the Software.
 17 | 
 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 25 | """
 26 | 
 27 | import bigtestlib
 28 | bigtestlib.preload_local_big()
 29 | 
 30 | import big.all as big
 31 | import unittest
 32 | 
 33 | 
 34 | original_values = [
 35 |     5,
 36 |     1,
 37 |     10,
 38 |     2,
 39 |     20,
 40 |     7,
 41 |     15,
 42 |     25,
 43 |     ]
 44 | 
 45 | 
 46 | class BigTests(unittest.TestCase):
 47 |     def test_heap_basics(self):
 48 |         h = big.Heap()
 49 |         for value in original_values:
 50 |             h.append(value)
 51 |         values = []
 52 |         while h:
 53 |             values.append(h.popleft())
 54 |         sorted_values = list(sorted(values))
 55 |         self.assertEqual(values, sorted_values)
 56 | 
 57 |     def test_heap_preinitialize_and_iteration(self):
 58 |         h2 = big.Heap(original_values)
 59 |         self.assertEqual(len(h2), len(original_values))
 60 |         values = list(h2)
 61 |         sorted_values = list(sorted(values))
 62 |         self.assertEqual(values, sorted_values)
 63 | 
 64 |     def test_dont_modify_during_iteration(self):
 65 |         h = big.Heap(original_values)
 66 |         with self.assertRaises(RuntimeError):
 67 |             for value in h:
 68 |                 h.append(45)
 69 |         with self.assertRaises(RuntimeError):
 70 |             for value in h:
 71 |                 h.extend([44, 55, 66])
 72 |         with self.assertRaises(RuntimeError):
 73 |             for value in h:
 74 |                 h.remove(20)
 75 |         with self.assertRaises(RuntimeError):
 76 |             for value in h:
 77 |                 h.popleft()
 78 |         with self.assertRaises(RuntimeError):
 79 |             for value in h:
 80 |                 h.popleft_and_append(33)
 81 |         with self.assertRaises(RuntimeError):
 82 |             for value in h:
 83 |                 h.append_and_popleft(33)
 84 |         with self.assertRaises(RuntimeError):
 85 |             for value in h:
 86 |                 h.clear()
 87 | 
 88 |     def test_random_one_liner_methods(self):
 89 |         h = big.Heap()
 90 |         self.assertEqual(len(h), 0)
 91 |         self.assertFalse(h)
 92 |         h.extend(original_values)
 93 |         self.assertEqual(len(h), len(original_values))
 94 |         self.assertTrue(h)
 95 |         self.assertTrue(5 in h)
 96 |         self.assertFalse(6 in h)
 97 | 
 98 |         self.assertEqual(h[:3], [1, 2, 5])
 99 |         self.assertEqual(h[:-5], [1, 2, 5])
100 |         self.assertEqual(h[-3:], [15, 20, 25])
101 |         self.assertEqual(h[5:], [15, 20, 25])
102 |         self.assertEqual(h[::2], [1, 5, 10, 20])
103 | 
104 |         h2 = h.copy()
105 |         self.assertEqual(h, h2)
106 |         self.assertEqual(h.queue, h2.queue)
107 |         h2 = big.Heap(original_values)
108 |         self.assertEqual(h, h2)
109 |         self.assertEqual(h.queue, h2.queue)
110 | 
111 |         h.clear()
112 |         self.assertEqual(len(h), 0)
113 |         self.assertFalse(h)
114 | 
115 |         h2 = big.Heap()
116 |         self.assertEqual(h, h2)
117 | 
118 |         h.extend((3, 1, 2))
119 |         self.assertEqual(list(h), [1, 2, 3])
120 |         h.remove(2)
121 |         self.assertEqual(list(h), [1, 3])
122 |         o = h.append_and_popleft(4)
123 |         self.assertEqual(o, 1)
124 |         self.assertEqual(list(h), [3, 4])
125 |         o = h.append_and_popleft(2)
126 |         self.assertEqual(o, 2)
127 |         self.assertEqual(list(h), [3, 4])
128 |         o = h.popleft_and_append(2)
129 |         self.assertEqual(o, 3)
130 |         self.assertEqual(list(h), [2, 4])
131 | 
132 |         with self.assertRaises(TypeError):
133 |             h[1.5]
134 |         with self.assertRaises(TypeError):
135 |             h['abc']
136 | 
137 | 
138 | 
139 |     def test_getitem(self):
140 |         h = big.Heap(original_values)
141 |         self.assertEqual(h[0], 1)
142 |         self.assertEqual(h[1], 2)
143 |         self.assertEqual(h[-1], 25)
144 |         self.assertEqual(h[-2], 20)
145 |         self.assertEqual(h[0:4], [1, 2, 5, 7])
146 |         self.assertEqual(h[-4:], [10, 15, 20, 25])
147 |         sorted_values = sorted(original_values)
148 |         self.assertEqual(h[:], sorted_values)
149 |         self.assertEqual(h[:], h.queue)
150 | 
151 |     def test_reprs(self):
152 |         h = big.Heap(original_values)
153 |         self.assertEqual(repr(h)[:6], '")
78 | 
79 |     def test_dunder_next_raising_stop_iteration(self):
80 |         # A little white-box testing here to make coverage happy.
81 |         # In order to get __next__ to specifically raise StopIteration,
82 |         # we have to use pbi.next to empty out the iterator.
83 |         pbi = big.PushbackIterator((0,))
84 |         sentinel = object()
85 |         self.assertEqual(pbi.next(sentinel), 0)
86 |         self.assertEqual(pbi.next(sentinel), sentinel)
87 |         for i in pbi:
88 |             self.assertEqual(True, False, "shouldn't reach here! pbi is exhausted!") # pragma: no cover
89 | 
90 | 
91 | def run_tests():
92 |     bigtestlib.run(name="big.itertools", module=__name__)
93 | 
94 | if __name__ == "__main__": # pragma: no cover
95 |     run_tests()
96 |     bigtestlib.finish()
97 | 


--------------------------------------------------------------------------------
/tests/test_log.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | 
  3 | _license = """
  4 | big
  5 | Copyright 2022-2024 Larry Hastings
  6 | All rights reserved.
  7 | 
  8 | Permission is hereby granted, free of charge, to any person obtaining a
  9 | copy of this software and associated documentation files (the "Software"),
 10 | to deal in the Software without restriction, including without limitation
 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
 12 | and/or sell copies of the Software, and to permit persons to whom the
 13 | Software is furnished to do so, subject to the following conditions:
 14 | 
 15 | The above copyright notice and this permission notice shall be included
 16 | in all copies or substantial portions of the Software.
 17 | 
 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 25 | """
 26 | 
 27 | import bigtestlib
 28 | bigtestlib.preload_local_big()
 29 | 
 30 | import builtins
 31 | import big.all as big
 32 | import unittest
 33 | import time
 34 | 
 35 | def fake_clock():
 36 |     def fake_clock():
 37 |         time = 0
 38 |         while True:
 39 |             yield time
 40 |             time += 12_000_000 # add twelve milliseconds
 41 |     return fake_clock().__next__
 42 | 
 43 | class BigTestLog(unittest.TestCase):
 44 | 
 45 |     maxDiff=None
 46 | 
 47 |     def test_smoke_test_log(self):
 48 |         clock = fake_clock()
 49 |         log = big.Log(clock=clock)
 50 | 
 51 |         log.reset()
 52 |         log.enter("subsystem")
 53 |         log('event 1')
 54 |         clock()
 55 |         log('event 2')
 56 |         log.exit()
 57 |         got = []
 58 |         log.print(print=got.append, fractional_width=3)
 59 | 
 60 |         expected = """
 61 | [event log]
 62 |   start   elapsed  event
 63 |   ------  ------  ---------------
 64 |   00.000  00.012  log start
 65 |   00.012  00.012  subsystem start
 66 |   00.024  00.024    event 1
 67 |   00.048  00.012    event 2
 68 |   00.060  00.000  subsystem end
 69 |         """.strip().split('\n')
 70 | 
 71 |         self.assertEqual(expected, got)
 72 | 
 73 |         i_got = iter(got)
 74 |         i_expected = iter(expected)
 75 | 
 76 |         per_line_elapsed = [
 77 |             0.012,
 78 |             0.012,
 79 |             0.024,
 80 |             0.012,
 81 |             0.0
 82 |             ]
 83 | 
 84 |         def split_line(s):
 85 |             s = s.strip()
 86 |             start_time, _, s = s.partition("  ")
 87 |             assert _
 88 |             elapsed_time, _, s = s.partition("  ")
 89 |             assert _
 90 |             return (float(start_time), float(elapsed_time), s)
 91 | 
 92 |         expected_start_time = 0
 93 |         for line_number, (expected, got, expected_elapsed_time) in enumerate(zip(i_expected, i_got, per_line_elapsed)):
 94 |             if line_number < 3:
 95 |                 self.assertEqual(expected, got)
 96 |                 continue
 97 |             _, _, expected_event = split_line(expected)
 98 |             got_start_time, got_elapsed_time, got_event = split_line(got)
 99 |             self.assertEqual(got_start_time, expected_start_time, f"error on log line {line_number}: start time didn't match! expected {expected_start_time}, got {got_start_time}")
100 |             self.assertGreaterEqual(got_elapsed_time, expected_elapsed_time, f"error on log line {line_number}: elapsed time didn't match! expected {expected_elapsed_time}, got {got_elapsed_time}")
101 |             self.assertEqual(got_event, expected_event, f"error on log line {line_number}: event didn't match! expected {expected_event!r}, got {got_event!r}")
102 |             expected_start_time += got_elapsed_time
103 | 
104 |     def test_default_settings(self):
105 |         log = big.Log()
106 |         log('event 1')
107 |         buffer = []
108 |         real_print = builtins.print
109 |         builtins.print = buffer.append
110 |         log.print()
111 |         builtins.print = real_print
112 |         got = "\n".join(buffer)
113 |         self.assertIn("event 1", got)
114 | 
115 | 
116 | def run_tests():
117 |     bigtestlib.run(name="big.log", module=__name__)
118 | 
119 | if __name__ == "__main__": # pragma: no cover
120 |     run_tests()
121 |     bigtestlib.finish()
122 | 


--------------------------------------------------------------------------------
/tests/test_metadata.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python3
 2 | 
 3 | _license = """
 4 | big
 5 | Copyright 2022-2024 Larry Hastings
 6 | All rights reserved.
 7 | 
 8 | Permission is hereby granted, free of charge, to any person obtaining a
 9 | copy of this software and associated documentation files (the "Software"),
10 | to deal in the Software without restriction, including without limitation
11 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 | and/or sell copies of the Software, and to permit persons to whom the
13 | Software is furnished to do so, subject to the following conditions:
14 | 
15 | The above copyright notice and this permission notice shall be included
16 | in all copies or substantial portions of the Software.
17 | 
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 | """
26 | 
27 | import bigtestlib
28 | bigtestlib.preload_local_big()
29 | 
30 | import big.all as big
31 | import unittest
32 | 
33 | 
34 | class BigTestVersion(unittest.TestCase):
35 | 
36 |     def test_metadata(self):
37 |         self.assertTrue(big.__version__)
38 |         self.assertIsInstance(big.__version__, str)
39 | 
40 |         vt = big.metadata.version
41 |         self.assertTrue(vt)
42 |         self.assertIsInstance(vt, big.version.Version)
43 |         self.assertEqual(vt, big.version.Version(big.__version__))
44 |         self.assertEqual(str(vt), big.__version__)
45 | 
46 | 
47 | def run_tests():
48 |     bigtestlib.run(name="big.metadata", module=__name__)
49 | 
50 | if __name__ == "__main__": # pragma: no cover
51 |     run_tests()
52 |     bigtestlib.finish()
53 | 


--------------------------------------------------------------------------------
/tests/test_search_path/empty_path/this_file_doesnt_match_anything:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larryhastings/big/d91ffb12841e5970cf07262e9d7ecbe37bdc8b91/tests/test_search_path/empty_path/this_file_doesnt_match_anything


--------------------------------------------------------------------------------
/tests/test_search_path/file_without_extension/foobar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larryhastings/big/d91ffb12841e5970cf07262e9d7ecbe37bdc8b91/tests/test_search_path/file_without_extension/foobar


--------------------------------------------------------------------------------
/tests/test_search_path/foo_d_path/foo.d/file.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larryhastings/big/d91ffb12841e5970cf07262e9d7ecbe37bdc8b91/tests/test_search_path/foo_d_path/foo.d/file.txt


--------------------------------------------------------------------------------
/tests/test_search_path/foo_h_path/foo.h:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larryhastings/big/d91ffb12841e5970cf07262e9d7ecbe37bdc8b91/tests/test_search_path/foo_h_path/foo.h


--------------------------------------------------------------------------------
/tests/test_search_path/foo_h_path/foo.hpp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larryhastings/big/d91ffb12841e5970cf07262e9d7ecbe37bdc8b91/tests/test_search_path/foo_h_path/foo.hpp


--------------------------------------------------------------------------------
/tests/test_search_path/want_directories/mydir/file:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larryhastings/big/d91ffb12841e5970cf07262e9d7ecbe37bdc8b91/tests/test_search_path/want_directories/mydir/file


--------------------------------------------------------------------------------
/tests/test_time.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | 
  3 | _license = """
  4 | big
  5 | Copyright 2022-2024 Larry Hastings
  6 | All rights reserved.
  7 | 
  8 | Permission is hereby granted, free of charge, to any person obtaining a
  9 | copy of this software and associated documentation files (the "Software"),
 10 | to deal in the Software without restriction, including without limitation
 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
 12 | and/or sell copies of the Software, and to permit persons to whom the
 13 | Software is furnished to do so, subject to the following conditions:
 14 | 
 15 | The above copyright notice and this permission notice shall be included
 16 | in all copies or substantial portions of the Software.
 17 | 
 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 25 | """
 26 | 
 27 | import bigtestlib
 28 | bigtestlib.preload_local_big()
 29 | 
 30 | import big.all as big
 31 | import datetime
 32 | import re
 33 | import time
 34 | import unittest
 35 | 
 36 | 
 37 | class BigTests(unittest.TestCase):
 38 | 
 39 |     def test_timestamp_human(self):
 40 |         human_re = re.compile(r"^(\d\d\d\d)/\d\d/\d\d \d\d:\d\d:\d\d(\.\d\d\d\d\d\d)?$")
 41 |         for t in (
 42 |             big.timestamp_human(),
 43 |             big.timestamp_human(None),
 44 |             ):
 45 |             self.assertTrue(t)
 46 |             match = human_re.match(t)
 47 |             self.assertTrue(match)
 48 |             self.assertGreaterEqual(int(match.group(1)), 2022)
 49 | 
 50 |         with self.assertRaises(TypeError):
 51 |             big.timestamp_human('abcde')
 52 | 
 53 |         # Q: timestamp_human deliberately uses the local time zone.
 54 |         # but specifying the time using an int or float (in any of
 55 |         # a number of ways) is in UTC.  how do we figure out the
 56 |         # correct UTC time so that we can get a consistent time
 57 |         # (e.g. Jan 1st 1970)
 58 |         #
 59 |         # A: pick a time, e.g. the epoch.  create a datetime object
 60 |         # set at that time, and ask it what the UTC offset is for
 61 |         # that moment.  then subtract that offset from the UTC int/float
 62 |         # time.  that gives you the UTC int/float time that renders
 63 |         # in the local timezone for the value you want.
 64 |         zero = 0
 65 |         aware = datetime.datetime(1970, 1, 1).astimezone()
 66 |         if aware.tzinfo:
 67 |             zero -= int(aware.utcoffset().total_seconds())
 68 |             float_zero = float(zero)
 69 | 
 70 |             epoch = "1970/01/01 00:00:00"
 71 |             epoch_with_microseconds = f"{epoch}.000000"
 72 |             self.assertEqual(big.timestamp_human(zero), epoch)
 73 |             self.assertEqual(big.timestamp_human(float_zero), epoch_with_microseconds)
 74 |             self.assertEqual(big.timestamp_human(time.gmtime(float_zero)), epoch)
 75 |             self.assertEqual(big.timestamp_human(time.localtime(float_zero)), epoch)
 76 |             self.assertEqual(big.timestamp_human(datetime.datetime.fromtimestamp(zero)), epoch_with_microseconds)
 77 |             self.assertEqual(big.timestamp_human(datetime.datetime.fromtimestamp(zero, datetime.timezone.utc)), epoch_with_microseconds)
 78 | 
 79 |             self.assertEqual(big.timestamp_human(datetime.datetime(1234, 5, 6, 7, 8, 9, microsecond=123456)), "1234/05/06 07:08:09.123456")
 80 | 
 81 |     def test_timestamp_3339Z(self):
 82 |         re_3339z = re.compile(r"^(\d\d\d\d)-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d\d\d\d)?Z$")
 83 |         for t in (
 84 |             big.timestamp_3339Z(),
 85 |             big.timestamp_3339Z(None),
 86 |             ):
 87 |             self.assertTrue(t)
 88 |             match = re_3339z.match(t)
 89 |             self.assertTrue(match)
 90 |             self.assertGreaterEqual(int(match.group(1)), 2022)
 91 | 
 92 |         epoch = "1970-01-01T00:00:00Z"
 93 |         epoch_with_microseconds = epoch.replace('Z', ".000000Z")
 94 |         self.assertEqual(big.timestamp_3339Z(0), epoch)
 95 |         self.assertEqual(big.timestamp_3339Z(0.0), epoch)
 96 |         self.assertEqual(big.timestamp_3339Z(time.gmtime(0.0)), epoch)
 97 |         self.assertEqual(big.timestamp_3339Z(time.localtime(0.0)), epoch)
 98 |         self.assertEqual(big.timestamp_3339Z(datetime.datetime.fromtimestamp(0)), epoch_with_microseconds)
 99 |         self.assertEqual(big.timestamp_3339Z(datetime.datetime.fromtimestamp(0, datetime.timezone.utc)), epoch_with_microseconds)
100 | 
101 |         self.assertEqual(big.timestamp_3339Z(datetime.datetime(1234, 5, 6, 7, 8, 9, tzinfo=datetime.timezone.utc, microsecond=123456)), "1234-05-06T07:08:09.123456Z")
102 | 
103 |         with self.assertRaises(TypeError):
104 |             big.timestamp_3339Z('abcde')
105 | 
106 |     try:
107 |         from big.time import parse_timestamp_3339Z
108 |         def test_parse_timestamp_3339Z(self):
109 |             utc = datetime.timezone.utc
110 |             datetimes = []
111 |             for seconds in range(2):
112 |                 datetimes.append(datetime.datetime(1970, 1, 1, 0, 0, seconds, tzinfo=utc))
113 |             self.assertEqual(big.parse_timestamp_3339Z("1970-01-01T00:00:00Z"), datetimes[0])
114 |             self.assertEqual(big.parse_timestamp_3339Z("1970-01-01T00:00:00.000000Z"), datetimes[0])
115 |             self.assertEqual(big.parse_timestamp_3339Z("1970-01-01T00:00:01.000000Z"), datetimes[1])
116 |             self.assertEqual(big.parse_timestamp_3339Z("2022-05-29T05:40:24Z"), datetime.datetime(2022, 5, 29, 5, 40, 24, tzinfo=datetime.timezone.utc))
117 | 
118 |             naive = datetime.datetime(1970, 1, 1, 0, 0, 0)
119 |             self.assertEqual(big.parse_timestamp_3339Z("1970-01-01T00:00:00.000000"), naive)
120 |             aware = big.datetime_set_timezone(naive, utc)
121 |             self.assertEqual(big.parse_timestamp_3339Z("1970-01-01T00:00:00.000000Z"), aware)
122 |             self.assertEqual(aware, big.datetime_ensure_timezone(naive, utc))
123 |             self.assertEqual(aware, big.datetime_ensure_timezone(aware, utc))
124 |     except ImportError: # pragma: no cover
125 |         pass
126 | 
127 | 
128 | def run_tests():
129 |     bigtestlib.run(name="big.time", module=__name__)
130 | 
131 | if __name__ == "__main__": # pragma: no cover
132 |     run_tests()
133 |     bigtestlib.finish()
134 | 


--------------------------------------------------------------------------------
/tests/test_version.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | 
  3 | _license = """
  4 | big
  5 | Copyright 2022-2024 Larry Hastings
  6 | All rights reserved.
  7 | 
  8 | Permission is hereby granted, free of charge, to any person obtaining a
  9 | copy of this software and associated documentation files (the "Software"),
 10 | to deal in the Software without restriction, including without limitation
 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
 12 | and/or sell copies of the Software, and to permit persons to whom the
 13 | Software is furnished to do so, subject to the following conditions:
 14 | 
 15 | The above copyright notice and this permission notice shall be included
 16 | in all copies or substantial portions of the Software.
 17 | 
 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 22 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 24 | THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 25 | """
 26 | 
 27 | import bigtestlib
 28 | bigtestlib.preload_local_big()
 29 | 
 30 | import big.all as big
 31 | import pprint
 32 | import random
 33 | import sys
 34 | import unittest
 35 | 
 36 | 
 37 | V = Version = big.Version
 38 | 
 39 | 
 40 | class BigTestVersion(unittest.TestCase):
 41 | 
 42 |     def test_parsing(self):
 43 |         def fail(s):
 44 |             with self.assertRaises(ValueError):
 45 |                 Version(s)
 46 | 
 47 |         fail("!")
 48 |         fail("-3")
 49 |         fail("3.")
 50 |         fail("3.-4")
 51 |         fail("3.14.")
 52 |         fail("3.14.-15")
 53 |         fail("3.14.15alpha!0")
 54 |         fail("3.14.15alpha/0")
 55 |         fail("3.14.15alpha0!")
 56 |         fail("3.14.15alpha0.")
 57 |         fail("3.14.15alpha0x")
 58 |         fail("3.14.15alphax0")
 59 |         fail("3.14.15blah")
 60 |         fail("3.14.15final")
 61 |         fail("3.14.15x")
 62 |         fail("3.14x")
 63 |         fail("3x")
 64 |         fail("x")
 65 |         fail("x3")
 66 | 
 67 |         def work(s):
 68 |             self.assertIsInstance(Version(s), Version)
 69 | 
 70 |         work("0")
 71 |         work("03")
 72 |         work("3")
 73 |         work("3.0")
 74 |         work("3.04")
 75 |         work("3.14.15")
 76 |         work("3.14.15alpha")
 77 |         work("3.14.15alpha0")
 78 |         work("3.14.15alpha022")
 79 |         work("3.14.15alpha05")
 80 |         work("3.14.15alpha22")
 81 |         work("3.14.15alpha5")
 82 |         work("3.14.15beta")
 83 |         work("3.14.15rc")
 84 |         work("3.4")
 85 | 
 86 |     def test_version_info(self):
 87 |         # compute a version object from Python's version the easy way
 88 |         easy = V(sys.version_info)
 89 |         self.assertEqual(easy, sys.version_info)
 90 | 
 91 |         less = V(f"{sys.version_info.major}.{sys.version_info.minor - 1}")
 92 |         self.assertLess(less, sys.version_info)
 93 | 
 94 |         greater = V(f"{sys.version_info.major}.{sys.version_info.minor + 1}")
 95 |         self.assertGreater(greater, sys.version_info)
 96 | 
 97 |         # now do it the hard way
 98 |         release = [sys.version_info.major, sys.version_info.minor]
 99 |         if sys.version_info.micro:
100 |             release.append(sys.version_info.micro)
101 |         release = tuple(release)
102 | 
103 |         kwargs = {}
104 |         release_level = big.version._sys_version_info_release_level_normalize.get(sys.version_info.releaselevel, sys.version_info.releaselevel)
105 |         if release_level: # pragma: nocover
106 |             kwargs['release_level'] = release_level
107 |         if sys.version_info.serial: # pragma: nocover
108 |             kwargs['serial'] = sys.version_info.serial
109 | 
110 |         hard = V(release=release, **kwargs)
111 | 
112 |         self.assertEqual(easy, hard)
113 | 
114 |     def test_packaging_version(self):
115 |         try: # pragma: nocover
116 |             from packaging.version import Version as PV
117 | 
118 |             pv135 = PV('1.3.5')
119 |             v = V(pv135)
120 |             self.assertEqual(v, pv135)
121 |             pv136 = PV('1.3.6')
122 |             self.assertLess(v, pv136)
123 | 
124 |         except ImportError: # pragma: nocover
125 |             pass
126 | 
127 | 
128 |     def test_normalize(self):
129 |         def test(v1, v2):
130 |             v1 = Version(v1)
131 |             v2 = Version(v2)
132 |             self.assertEqual(v1, v2)
133 |         test('1.0.1', '1.0.1.0')
134 |         test('1.0.1', '1.0.1.0.0.0.0.0.0')
135 |         test('01.0.1', '1.0.1.0.0')
136 |         test('01.0.0001.0', '1.0.1')
137 | 
138 |         test('15.23alpha1', '15.23a1')
139 |         test('15.23beta1', '15.23beta1')
140 |         test('15.23rc1', '15.23c1')
141 | 
142 |         self.assertEqual(
143 |             V('1!2.3rc45.post67.dev89+i.am.the.eggman'),
144 |             V(epoch=1, release=(2, 3), release_level='rc', serial=45, post=67, dev=89, local=('i', 'am', 'the', 'eggman'))
145 |             )
146 | 
147 |         self.assertEqual(
148 |             V('4.5.0b6'),
149 |             V(release=(4, 5), release_level='beta', serial=6)
150 |             )
151 | 
152 |         self.assertEqual(
153 |             V('88'),
154 |             V(release=(88,))
155 |             )
156 | 
157 | 
158 |     def test_comparison(self):
159 | 
160 |         # here's a list of version, already in sorted order.
161 |         sorted_versions = [
162 |             V('1.0a1'),
163 |             V('1.0a2.dev456'),
164 |             V('1.0a2'),
165 |             V('1.0a2-456'), # post
166 |             V('1.0b1.dev456'),
167 |             V('1.0b2'),
168 |             V('1.0b2.post345'),
169 |             V('1.0c1.dev456'),
170 |             V('1.0rc1'),
171 |             V('1.0.dev456'),
172 |             V('1.0'),
173 |             V('1.0.post456.dev34'),
174 |             V('1.0.post456.dev34+abc.123'),
175 |             V('1.0.post456.dev34+abc.124'),
176 |             V('1.0.post456.dev34+abd'),
177 |             V('1.0.post456.dev34+abd.123'),
178 |             V('1.0.post456.dev34+1'),
179 |             V('1.0.post456'),
180 |             V('1.0.1'),
181 |             V('1.0.1.1'),
182 |             V('1.0.2'),
183 |             V('1!0.0.0.0.1'),
184 |             V('2!0.0.0.0.0.1'),
185 |             ]
186 | 
187 |         # first--let's test round-tripping through repr!
188 |         for v in sorted_versions:
189 |             r = repr(v)
190 |             v2 = eval(r)
191 |             self.assertEqual(v, v2)
192 | 
193 |         # check that the first version is < every other version in the list
194 |         v1 = sorted_versions[0]
195 |         for v2 in sorted_versions[1:]:
196 |             self.assertLess(v1, v2)
197 | 
198 |         # check transitivity: every version is < the entry that is  ahead in the list
199 |         for delta in (1, 2, 3, 5, 7):
200 |             for i in range(len(sorted_versions) - delta):
201 |                 self.assertLess(sorted_versions[i], sorted_versions[i + delta])
202 | 
203 |         # scramble a copy of the array, using a couple fixed seeds,
204 |         # then sort it and confirm that the array is identical to the original (sorted) array
205 |         for seed in (
206 |             'seed1',
207 |             'seed2',
208 |             "T'was brillig, and the slithey toves",
209 |             "Lookin' over their shoulder for me", # Stan Ridgway, "Newspapers"
210 |             "And I've tasted the strongest meats / And laid them down in golden sheets", # Peter Gabriel, "Back In NYC"
211 |             "And if I want more love in the world / I must show more love to myself / 'Cause I want to change the world", # Todd Rundgren, "Change Myself"
212 |             "My momma tells me every day, not to move so fast across the room", # "Shorty And The EZ Mouse"
213 |             ):
214 |             r = random.Random(seed)
215 |             scrambled = sorted_versions.copy()
216 |             r.shuffle(scrambled)
217 |             got = list(scrambled)
218 |             got.sort()
219 | 
220 |             if 0:
221 |                 print()
222 |                 print("-" * 79)
223 |                 print("[scrambled]")
224 |                 pprint.pprint(scrambled)
225 |                 print()
226 |                 print("[expected]")
227 |                 pprint.pprint(sorted_versions)
228 |                 print()
229 |                 print("[got]")
230 |                 pprint.pprint(got)
231 | 
232 |             self.assertEqual(sorted_versions, got)
233 | 
234 |         v = sorted_versions[0]
235 |         self.assertNotEqual(v, 1.3)
236 |         self.assertNotEqual(v, "abc")
237 | 
238 |         with self.assertRaises(TypeError):
239 |             v < 1.3
240 |         with self.assertRaises(TypeError):
241 |             v < "abc"
242 | 
243 | 
244 |     def test_convert_to_string(self):
245 |         # test everything
246 |         s = '8!1.0.3rc5.post456.dev34+apple.cart.123'
247 |         v = V(s)
248 |         self.assertEqual(s, str(v))
249 |         self.assertEqual("Version('" + s + "')", repr(v))
250 | 
251 |     def test_format(self):
252 |         v = V('8!1.0.3rc5.post456.dev34+apple.cart.123')
253 |         self.assertEqual(v.format('{release}'), '1.0.3')
254 |         self.assertEqual(v.format('{epoch}'), '8')
255 |         self.assertEqual(v.format('{release_level}'), 'rc')
256 | 
257 | 
258 |     def test_input_validation(self):
259 |         with self.assertRaises(ValueError):
260 |             V()
261 |         with self.assertRaises(ValueError):
262 |             V((1, 3, 5))
263 |         with self.assertRaises(ValueError):
264 |             V(5)
265 |         with self.assertRaises(ValueError):
266 |             V(3.2)
267 |         with self.assertRaises(ValueError):
268 |             V(4+3j)
269 |         with self.assertRaises(ValueError):
270 |             V({1, 2, 3})
271 |         with self.assertRaises(ValueError):
272 |             V(b"1.3.5")
273 | 
274 |         with self.assertRaises(ValueError):
275 |             V(epoch='4', release=(1, 3, 5))
276 |         with self.assertRaises(ValueError):
277 |             V(epoch=-1, release=(1, 3, 5))
278 | 
279 |         with self.assertRaises(ValueError):
280 |             V(release=1)
281 |         with self.assertRaises(ValueError):
282 |             V(release=1.3)
283 |         with self.assertRaises(ValueError):
284 |             V(release=1+3j)
285 |         with self.assertRaises(ValueError):
286 |             V(release="1.3.5")
287 |         with self.assertRaises(ValueError):
288 |             V(release=[1, 3, 5])
289 |         with self.assertRaises(ValueError):
290 |             V(release=('1', '3', '5'))
291 |         with self.assertRaises(ValueError):
292 |             V(release=(1.0, 3.0, 5.0))
293 | 
294 |         with self.assertRaises(ValueError):
295 |             V(release_level='', release=(1, 3, 5))
296 |         with self.assertRaises(ValueError):
297 |             V(release_level='abc', release=(1, 3, 5))
298 |         with self.assertRaises(ValueError):
299 |             V(release_level=24, release=(1, 3, 5))
300 |         with self.assertRaises(ValueError):
301 |             V(release_level=-1, release=(1, 3, 5))
302 | 
303 |         with self.assertRaises(ValueError):
304 |             V(release_level='rc', serial='7', release=(1, 3, 5))
305 |         with self.assertRaises(ValueError):
306 |             V(release_level='rc', serial='', release=(1, 3, 5))
307 |         with self.assertRaises(ValueError):
308 |             V(release_level='rc', serial=-1, release=(1, 3, 5))
309 | 
310 |         with self.assertRaises(ValueError):
311 |             V(post='334', release=(1, 3, 5))
312 |         with self.assertRaises(ValueError):
313 |             V(post='', release=(1, 3, 5))
314 |         with self.assertRaises(ValueError):
315 |             V(post=-1, release=(1, 3, 5))
316 | 
317 |         with self.assertRaises(ValueError):
318 |             V(dev='556', release=(1, 3, 5))
319 |         with self.assertRaises(ValueError):
320 |             V(dev='', release=(1, 3, 5))
321 |         with self.assertRaises(ValueError):
322 |             V(dev=-1, release=(1, 3, 5))
323 | 
324 |         with self.assertRaises(ValueError):
325 |             V(local='abc', release=(1, 3, 5))
326 |         with self.assertRaises(ValueError):
327 |             V(local=1, release=(1, 3, 5))
328 |         with self.assertRaises(ValueError):
329 |             V(local=1.3, release=(1, 3, 5))
330 |         with self.assertRaises(ValueError):
331 |             V(local=["a", "33"], release=(1, 3, 5))
332 |         with self.assertRaises(ValueError):
333 |             V(local=("a", 33), release=(1, 3, 5))
334 |         with self.assertRaises(ValueError):
335 |             V(local=("a", '', 'c'), release=(1, 3, 5))
336 |         with self.assertRaises(ValueError):
337 |             V(local=("a", '33!', 'c'), release=(1, 3, 5))
338 |         with self.assertRaises(ValueError):
339 |             V(local=("a", ' 33', 'c'), release=(1, 3, 5))
340 | 
341 |         with self.assertRaises(ValueError):
342 |             V("1.3.5", epoch=1)
343 |         with self.assertRaises(ValueError):
344 |             V("1.3.5", release=(1, 3, 5))
345 |         with self.assertRaises(ValueError):
346 |             V("1.3.5", release_level='rc')
347 |         with self.assertRaises(ValueError):
348 |             V("1.3.5", serial=1)
349 |         with self.assertRaises(ValueError):
350 |             V("1.3.5", post=1)
351 |         with self.assertRaises(ValueError):
352 |             V("1.3.5", dev=1)
353 |         with self.assertRaises(ValueError):
354 |             V("1.3.5", local="one.apple.3")
355 | 
356 |     def test_hashability(self):
357 |         versions = set()
358 |         for s in """
359 |             1.1.22
360 |             2.5.post88
361 |             2.4.0
362 |             2.4.1
363 |             2.2.1.dev24
364 |             2.3.5rc3
365 |             2.4
366 |             2.3.5rc3
367 |             2.4.0.0.0
368 |             02.04.000
369 |             3!0.5
370 | 
371 |         """.strip().split():
372 |             v = V(s)
373 |             versions.add(v)
374 | 
375 |         got = list(versions)
376 |         got.sort()
377 | 
378 |         expected = [
379 |             V("1.1.22"),
380 |             V("2.2.1.dev24"),
381 |             V("2.3.5rc3"),
382 |             V("2.4.0"),
383 |             V("2.4.1"),
384 |             V("2.5.post88"),
385 |             V("3!0.5")
386 |             ]
387 |         if 0:
388 |             print(">> expected")
389 |             pprint.pprint(expected)
390 |             print()
391 |             print(">> got")
392 |             pprint.pprint(got)
393 |             print()
394 |         self.assertEqual(expected, got)
395 | 
396 |     def test_accessors(self):
397 |         v = V('7!22.33.44rc1.post77.dev22+apple.dumpling_gang-l33t')
398 |         self.assertEqual(v.epoch, 7)
399 |         self.assertEqual(v.release, (22, 33, 44))
400 |         self.assertEqual(v.major, 22)
401 |         self.assertEqual(v.minor, 33)
402 |         self.assertEqual(v.micro, 44)
403 |         self.assertEqual(v.release_level, "rc")
404 |         self.assertEqual(v.releaselevel, "rc")
405 |         self.assertEqual(v.serial, 1)
406 |         self.assertEqual(v.dev, 22)
407 |         self.assertEqual(v.post, 77)
408 |         self.assertEqual(v.local, ("apple", "dumpling", "gang", "l33t"))
409 | 
410 |         v = V('1.2')
411 |         self.assertEqual(v.epoch, None)
412 |         self.assertEqual(v.release, (1, 2))
413 |         self.assertEqual(v.major, 1)
414 |         self.assertEqual(v.minor, 2)
415 |         self.assertEqual(v.micro, 0)
416 |         self.assertEqual(v.release_level, "final")
417 |         self.assertEqual(v.releaselevel, "final")
418 |         self.assertEqual(v.serial, None)
419 |         self.assertEqual(v.dev, None)
420 |         self.assertEqual(v.post, None)
421 |         self.assertEqual(v.local, None)
422 | 
423 |         v = V('73')
424 |         self.assertEqual(v.major, 73)
425 |         self.assertEqual(v.minor, 0)
426 | 
427 | 
428 | def run_tests():
429 |     bigtestlib.run(name="big.version", module=__name__)
430 | 
431 | if __name__ == "__main__": # pragma: no cover
432 |     run_tests()
433 |     bigtestlib.finish()
434 | 


--------------------------------------------------------------------------------