├── .coveragerc ├── .gitignore ├── .readthedocs.yml ├── .travis.yml ├── AUTHORS.rst ├── CHANGELOG.rst ├── LICENSE.txt ├── README.md ├── docs ├── Makefile ├── _static │ └── .gitignore ├── authors.rst ├── changelog.rst ├── conf.py ├── index.rst ├── license.rst ├── readme.rst └── requirements.txt ├── examples ├── basic_ordered.py └── basic_random.py ├── ordered ├── __init__.py └── ordered.py ├── pyproject.toml ├── setup.cfg ├── setup.py ├── tests ├── basic_ordered.py ├── conftest.py ├── example_bottle.py ├── example_bottle_manual.py ├── example_oo.py ├── ordered_test.py ├── test_basic.py ├── test_bottles.py ├── test_endofframe.py ├── test_example_oo.py ├── test_global_var.py ├── test_partialorder.py ├── test_partialorder_oo.py └── test_partialorder_oo3.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | source = ordered 5 | # omit = bad_file.py 6 | 7 | [paths] 8 | source = 9 | ordered/ 10 | */site-packages/ 11 | 12 | [report] 13 | # Regexes for lines to exclude from consideration 14 | exclude_lines = 15 | # Have to re-enable the standard pragma 16 | pragma: no cover 17 | 18 | # Don't complain about missing debug-only code: 19 | def __repr__ 20 | if self\.debug 21 | 22 | # Don't complain if tests don't hit defensive assertion code: 23 | raise AssertionError 24 | raise NotImplementedError 25 | 26 | # Don't complain if non-runnable code isn't run: 27 | if 0: 28 | if __name__ == .__main__.: 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary and binary files 2 | *~ 3 | *.py[cod] 4 | *.so 5 | *.cfg 6 | !.isort.cfg 7 | !setup.cfg 8 | *.orig 9 | *.log 10 | *.pot 11 | __pycache__/* 12 | .cache/* 13 | .*.swp 14 | */.ipynb_checkpoints/* 15 | .DS_Store 16 | 17 | # Project files 18 | .ropeproject 19 | .project 20 | .pydevproject 21 | .settings 22 | .idea 23 | .vscode 24 | tags 25 | 26 | # Package files 27 | *.egg 28 | *.eggs/ 29 | .installed.cfg 30 | *.egg-info 31 | 32 | # Unittest and coverage 33 | htmlcov/* 34 | .coverage 35 | .coverage.* 36 | .tox 37 | junit*.xml 38 | coverage.xml 39 | .pytest_cache/ 40 | 41 | # Build and docs folder/files 42 | build/* 43 | dist/* 44 | sdist/* 45 | docs/api/* 46 | docs/_rst/* 47 | docs/_build/* 48 | cover/* 49 | MANIFEST 50 | 51 | # Per-project virtualenvs 52 | .venv*/ 53 | .conda*/ 54 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Build documentation in the docs/ directory with Sphinx 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | # Build documentation with MkDocs 12 | #mkdocs: 13 | # configuration: mkdocs.yml 14 | 15 | # Optionally build your docs in additional formats such as PDF 16 | formats: 17 | - pdf 18 | 19 | python: 20 | version: 3.8 21 | install: 22 | - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.8" 4 | install: pip install tox-travis 5 | script: tox -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributors 3 | ============ 4 | 5 | * Andrew Gree 6 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | Version 0.1 6 | =========== 7 | 8 | - Feature A added 9 | - FIX: nasty bug #1729 fixed 10 | - add your changes here! 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 CriticalHop Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/hyperc-ai/ordered.svg?branch=main)](https://travis-ci.com/hyperc-ai/ordered) 2 | [![PyPI version](https://badge.fury.io/py/ordered.svg)](https://badge.fury.io/py/ordered) 3 | [![PyPI os](https://img.shields.io/badge/os-linux-blue)](https://img.shields.io/badge/os-linux-blue) 4 | [![PyPI status](https://img.shields.io/pypi/status/ordered.svg)](https://pypi.python.org/pypi/ordered/) 5 | 6 | ![Twitter Follow](https://img.shields.io/twitter/follow/hyperc_ai?style=social) 7 | ![Twitter URL](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Fhyperc-ai%2Fordered) 8 | 9 | 10 | # Python module `ordered` 11 | 12 | `ordered` module is the opposite to `random` - it maintains order in the program. 13 | 14 | 15 | ```python 16 | import random 17 | x = 5 18 | def increase(): 19 | global x 20 | x += 7 21 | def decrease(): 22 | global x 23 | x -= 2 24 | 25 | while x != 22: 26 | random.choice([increase, decrease])() 27 | # takes long time to exit ... 28 | ``` 29 | vs. 30 | 31 | ```python 32 | import random, ordered 33 | x = 5 34 | def increase(): 35 | global x 36 | x += 7 37 | def decrease(): 38 | global x 39 | x -= 2 40 | 41 | with ordered.orderedcontext(): # entropy-controlled context 42 | while x != 22: 43 | random.choice([increase, decrease])() 44 | # exits immediately with correct result 45 | pass 46 | ``` 47 | 48 | Ordered contexts are environments of controlled entropy. Contexts allow you to control which portions of the program will be guaranteed to exit with minimum state-changing steps. Raising any exceptions is also avoided by providing the correct "anti-random" `choice()` results. 49 | 50 | # Usage 51 | 52 | `ordered` is a Python 3.8+ module. Use cases include automated decisionmaking, manufacturing control, robotics, automated design, automated programming and others. 53 | 54 | You describe the world as Python objects and state-modifying methods. Defining an entropy-controlled context allows you to set up a goal for the system to satisfy all constraints and reach the desired state. 55 | 56 | To define constraints you add `assert` statements in your code and inside ordered context. Then you add a function-calling loop `while : random.choice()(random.choice())`. To exit the context the engine will have to call correct functions with correct arguments and end up with a staisfying state (see examples below). 57 | 58 | ## Requirements 59 | 60 | - Linux (tested on Ubuntu 20.04+) 61 | - Python 3.8 in virtualenv 62 | - Recommended: PyPy compatible with Python 3.7+, installed globally. 63 | 64 | ```bash 65 | # In Python3.8 virtualenv on Linux: 66 | $ pip install ordered 67 | ``` 68 | 69 | ## Entropy Context Objects 70 | 71 | 72 | ```python 73 | # ... normal python code 74 | with ordered.orderedcontext(): 75 | # ... entropy-controlled context, guaranteed to exit without exceptions 76 | # ... normal python code 77 | ``` 78 | 79 | - _ordered_.**orderedcontext**() 80 | 81 | Return a context manager and enter the context. `SchedulingError` will be raised if exit is not possible. 82 | 83 | Inside ordered context functions `random.choice` and `ordered.choice` are equivalent and no randomness is possible. If `choice()` is called without parameters then `gc.get_objects()` (all objects in Python heap) is considered by default. 84 | 85 | Optional returned context object allows to set parameters and limits such as `timeout` and `max_states`. 86 | 87 | _**Warning:** not all Python features are currently supported and thus `ordered` might fail with internal exception. In this case a rewrite of user code is needed to remove the usage of unsupported features (such as I/O, lists and for loops.)_ 88 | 89 | _**Warning:** `ordered` requires all entropy-controlled code to be type-hinted._ 90 | 91 | ```python 92 | # ... 93 | def decrease(): 94 | global x 95 | assert x > 25 # when run inside context this excludes cases when x <= 25 96 | # thus increasing amount of overall steps needed to complete 97 | x -= 2 98 | # ... 99 | with ordered.orderedcontext(): # entropy-controlled context 100 | while x < 21: # exit if x >= 21 101 | random.choice([increase, decrease])() 102 | assert x < 23 # only x == 21 or 22 matches overall 103 | ``` 104 | 105 | ## _`ordered`_.`choice()` 106 | 107 | - _ordered_.**choice**(objects=None) 108 | 109 | Choose and return the object that maintains maximum order in the program (minimum entropy). Any exception increases entropy to infinity so choices leading to exceptions will be avoided. 110 | Inside the entropy controlled context, `random.choice` is equivalent to `ordered.choice` (and also `random.choices` in the sense that it may return any amount of parameters when used as argument-generator in `choice(*choice())`). 111 | 112 | `objects` is a list of objects to choose from. If `objects` is `None` then `gc.get_objects()` is assumed by default. 113 | 114 | _**Warning:** current implementation of `while ... ordered` loop is hard-coded to the form shown in examples. `while` loops with other statements than a single-line `choice()` are not supported. Add your code to other parts of context and/or functions and methods in your program_ 115 | 116 | 117 | ## _`ordered`_.`side_effect(lambda: )` 118 | 119 | - _ordered_.**side_effect**(lamdba=[lambda function]) 120 | 121 | Execute the supplied lambda function as a side-effect avoiding the compilation and subsequent effect analysis by `ordered`. This is useful when I/O is easier schdeuled right within the entropy-controlled part of the program or when you know that the code to be executed has no useful effect on the state of the problem of interest. 122 | 123 | side_effect may only be used when importred into global namespace using `from ordered import side_effect` 124 | 125 | ```python 126 | from ordered import side_effect 127 | 128 | def move(t: Truck, l: Location): 129 | "Move truck to any adjacent location" 130 | assert l in t.location.adjacent 131 | t.locaiton = l 132 | t.distance += 1 133 | side_effect(lambda: print(f"This {__name__} code can have any Python construct and is not analysed. Current value is {t.distance}")) 134 | ``` 135 | 136 | ## Examples: 137 | 138 | ### Object Oriented Code 139 | 140 | Preferred way of implementing software models with `ordered` is object-oriented: 141 | 142 | ```python 143 | import ordered 144 | 145 | class MyVars: 146 | x: int 147 | steps: int 148 | def __init__(self) -> None: 149 | self.x = 0 150 | self.steps = 0 151 | 152 | def plus_x(self): 153 | self.x += 3 154 | self.count_steps() 155 | 156 | def minus_x(self): 157 | self.x -= 2 158 | self.count_steps() 159 | 160 | def count_steps(self): 161 | self.steps += 1 162 | 163 | m = MyVars() 164 | m.x = 5 165 | with ordered.orderedcontext(): 166 | while m.x != 12: 167 | ordered.choice()() 168 | 169 | print("Steps:", steps) 170 | ``` 171 | 172 | 173 | ### Pouring problem 174 | 175 | A classic bottle pouring puzzle. You are in the possession of two bottles, one with a capacity of 3 litres and one with a capacity of 5 litres. Next to you is an infinitely large tub of water. You need to measure exactly 4 litres in one of the bottles. You are only allowed to entirely empty or fill the bottles. You can't fill them partially since there is no indication on the bottles saying how much liquid is in them. How do you measure exactly 4 litres? 176 | 177 | ```python 178 | from ordered import orderedcontext, choice 179 | class Bottle: 180 | volume: int 181 | fill: int 182 | def __init__(self, volume: int): 183 | self.volume = volume 184 | self.fill = 0 185 | def fill_in(self): 186 | self.fill += self.volume 187 | assert self.fill == self.volume 188 | def pour_out(self, bottle: "Bottle"): 189 | assert self != bottle 190 | can_fit = bottle.volume - bottle.fill 191 | sf = self.fill 192 | bf = bottle.fill 193 | if self.fill <= can_fit: 194 | bottle.fill += self.fill 195 | self.fill = 0 196 | assert self.fill == 0 197 | assert bottle.fill == bf + sf 198 | else: 199 | bottle.fill += can_fit 200 | self.fill -= can_fit 201 | assert bottle.fill == bottle.volume 202 | assert self.fill == sf - can_fit 203 | def empty(self): 204 | self.fill = 0 205 | b1 = Bottle(3) 206 | b2 = Bottle(5) 207 | with orderedcontext(): 208 | while b2.fill != 4: 209 | choice([Bottle])() 210 | pass 211 | ``` 212 | 213 | _**NOTE:** Be careful with importing from a module into global namespace and using `choice()()` without parameters in global scope. Current implementation load all global objects including the `orderedcontext` and `choice` and cause an error_ 214 | 215 | ### Learning a function 216 | 217 | `ordered` can be used 218 | 219 | ```python 220 | from ordered import choice, orderedcontext 221 | from dataclasses import dataclass 222 | 223 | @dataclass 224 | class Point: 225 | x: int 226 | y: int 227 | 228 | data = [Point(1,1), Point(2,4), Point(3,9)] 229 | 230 | # TODO: create_function creates a nonrandom function out of Node objects with `ordered.choice` 231 | # TODO: run_function runs a node tree with a value and returns result 232 | 233 | with orderedcontext(): 234 | f = create_function() 235 | for point in data: 236 | assert run_function(f, point.x) == point.y 237 | # context exit guarantees that create_function() constructs a correct function to describe input 238 | 239 | # TODO: approximate function learning example 240 | ``` 241 | 242 | ## Work-in-progress functions 243 | 244 | ### _`ordered`_.`relaxedcontext()` 245 | 246 | Guaranteed to find an exit. Modifies the program if required. 247 | 248 | ### Method _`ordered`_.`def(heap_in_out: List)` 249 | 250 | Defines a function from a list of input and output heaps. The more examples of heaps are supplied, the better is the function. 251 | 252 | # Status 253 | 254 | Although the system is in use by several industry organizations, `ordered` is under heavy development. Expect rapid changes in language support, performance and bugs. 255 | 256 | # Limitations 257 | 258 | ## Python Language 259 | 260 | Overall we have a relatively complete support of 'basic' use of object-oriented programming style. However, there are some hard limitaions and work-in-progress items that are yet to be documented. 261 | 262 | Try to avoid multiline code as we have several places where line continuation may break during compilation. 263 | 264 | Built-ins support is minimal. No I/O can be executed except for in explicit `side_effect()` calls. 265 | 266 | None of the "ordered data structures" are supported: this includes `list`, `dict` and `tuple`. Use `set` or create your own data structures based on objects and classes. 267 | 268 | Loops are not supported, including `while` and `for` besides the main `while..choice()` loop as described above - define your problem by creating functions that can be iteratively called by `while.. choice()` to overcome this. 269 | 270 | Support of missing features is a current work in progress. 271 | 272 | ## Integer Math 273 | 274 | Math implementation is simple and works up to count 20-50 depedning on available resources. Future development includes switching to register-based math and monotonic-increase heuristics to support any numbers. 275 | 276 | ## Symbolic Execution Performance 277 | 278 | Current implementaion of Python code compilation is naive and doesn't scale well. The simpler your code, the faster it will compile. Future development includes implementing smarter symboic execution heuristics, pre-calculated database and statistical methods. 279 | 280 | ## Model Universality 281 | 282 | Current model can efficiently handle a limited set of problem classes and might require significantly more resources than would be needed with a more complete model. HyperC team provides more complete models for specific industry per request. Future development includes adding a universal pruning based on statistical methods as amount of data available to HyperC team grows. 283 | 284 | # Science behind `ordered` 285 | 286 | `ordered` is based on translating a Python program to [AI planning](https://en.wikipedia.org/wiki/Automated_planning_and_scheduling) problem and uses a customized [fast-downward](http://www.fast-downward.org/) as a backend. Additionally, we're implementing machine learning and pre-computed matrices on various levels to vastly improve performance on larger problems. 287 | # Contributing 288 | 289 | For any questions and inquries please feel free contact Andrew Gree, . 290 | 291 | # Support 292 | 293 | Module `ordered` is maintained by HyperC team, https://hyperc.com (CriticalHop Inc.) and is implemented in multiple production envorinments. 294 | 295 | # Investor Relations 296 | 297 | HyperC is fundraising! Please contact at . 298 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | AUTODOCDIR = api 11 | 12 | # User-friendly check for sphinx-build 13 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $?), 1) 14 | $(error "The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/") 15 | endif 16 | 17 | .PHONY: help clean Makefile 18 | 19 | # Put it first so that "make" without argument is like "make help". 20 | help: 21 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | 23 | clean: 24 | rm -rf $(BUILDDIR)/* $(AUTODOCDIR) 25 | 26 | # Catch-all target: route all unknown targets to Sphinx using the new 27 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 28 | %: Makefile 29 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 30 | -------------------------------------------------------------------------------- /docs/_static/.gitignore: -------------------------------------------------------------------------------- 1 | # Empty directory 2 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. _authors: 2 | .. include:: ../AUTHORS.rst 3 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changes: 2 | .. include:: ../CHANGELOG.rst 3 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # This file is execfile()d with the current directory set to its containing dir. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | # 7 | # All configuration values have a default; values that are commented out 8 | # serve to show the default. 9 | 10 | import os 11 | import sys 12 | import inspect 13 | import shutil 14 | 15 | # -- Path setup -------------------------------------------------------------- 16 | 17 | __location__ = os.path.join( 18 | os.getcwd(), os.path.dirname(inspect.getfile(inspect.currentframe())) 19 | ) 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | sys.path.insert(0, os.path.join(__location__, "../ordered")) 25 | 26 | # -- Run sphinx-apidoc ------------------------------------------------------- 27 | # This hack is necessary since RTD does not issue `sphinx-apidoc` before running 28 | # `sphinx-build -b html . _build/html`. See Issue: 29 | # https://github.com/rtfd/readthedocs.org/issues/1139 30 | # DON'T FORGET: Check the box "Install your project inside a virtualenv using 31 | # setup.py install" in the RTD Advanced Settings. 32 | # Additionally it helps us to avoid running apidoc manually 33 | 34 | try: # for Sphinx >= 1.7 35 | from sphinx.ext import apidoc 36 | except ImportError: 37 | from sphinx import apidoc 38 | 39 | output_dir = os.path.join(__location__, "api") 40 | module_dir = os.path.join(__location__, "../ordered") 41 | try: 42 | shutil.rmtree(output_dir) 43 | except FileNotFoundError: 44 | pass 45 | 46 | try: 47 | import sphinx 48 | 49 | cmd_line_template = ( 50 | "sphinx-apidoc --implicit-namespaces -f -o {outputdir} {moduledir}" 51 | ) 52 | cmd_line = cmd_line_template.format(outputdir=output_dir, moduledir=module_dir) 53 | 54 | args = cmd_line.split(" ") 55 | if tuple(sphinx.__version__.split(".")) >= ("1", "7"): 56 | # This is a rudimentary parse_version to avoid external dependencies 57 | args = args[1:] 58 | 59 | apidoc.main(args) 60 | except Exception as e: 61 | print("Running `sphinx-apidoc` failed!\n{}".format(e)) 62 | 63 | # -- General configuration --------------------------------------------------- 64 | 65 | # If your documentation needs a minimal Sphinx version, state it here. 66 | # needs_sphinx = '1.0' 67 | 68 | # Add any Sphinx extension module names here, as strings. They can be extensions 69 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 70 | extensions = [ 71 | "sphinx.ext.autodoc", 72 | "sphinx.ext.intersphinx", 73 | "sphinx.ext.todo", 74 | "sphinx.ext.autosummary", 75 | "sphinx.ext.viewcode", 76 | "sphinx.ext.coverage", 77 | "sphinx.ext.doctest", 78 | "sphinx.ext.ifconfig", 79 | "sphinx.ext.mathjax", 80 | "sphinx.ext.napoleon", 81 | "m2r2" 82 | ] 83 | 84 | # Add any paths that contain templates here, relative to this directory. 85 | templates_path = ["_templates"] 86 | 87 | # The suffix of source filenames. 88 | source_suffix = [".rst", ".md"] 89 | 90 | # The encoding of source files. 91 | # source_encoding = 'utf-8-sig' 92 | 93 | # The master toctree document. 94 | master_doc = "index" 95 | 96 | # General information about the project. 97 | project = "ordered" 98 | copyright = "2021, CriticalHop Inc." 99 | 100 | # The version info for the project you're documenting, acts as replacement for 101 | # |version| and |release|, also used in various other places throughout the 102 | # built documents. 103 | # 104 | # The short X.Y version. 105 | version = "" # Is set by calling `setup.py docs` 106 | # The full version, including alpha/beta/rc tags. 107 | release = "" # Is set by calling `setup.py docs` 108 | 109 | # The language for content autogenerated by Sphinx. Refer to documentation 110 | # for a list of supported languages. 111 | # language = None 112 | 113 | # There are two options for replacing |today|: either, you set today to some 114 | # non-false value, then it is used: 115 | # today = '' 116 | # Else, today_fmt is used as the format for a strftime call. 117 | # today_fmt = '%B %d, %Y' 118 | 119 | # List of patterns, relative to source directory, that match files and 120 | # directories to ignore when looking for source files. 121 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".venv"] 122 | 123 | # The reST default role (used for this markup: `text`) to use for all documents. 124 | # default_role = None 125 | 126 | # If true, '()' will be appended to :func: etc. cross-reference text. 127 | # add_function_parentheses = True 128 | 129 | # If true, the current module name will be prepended to all description 130 | # unit titles (such as .. function::). 131 | # add_module_names = True 132 | 133 | # If true, sectionauthor and moduleauthor directives will be shown in the 134 | # output. They are ignored by default. 135 | # show_authors = False 136 | 137 | # The name of the Pygments (syntax highlighting) style to use. 138 | pygments_style = "sphinx" 139 | 140 | # A list of ignored prefixes for module index sorting. 141 | # modindex_common_prefix = [] 142 | 143 | # If true, keep warnings as "system message" paragraphs in the built documents. 144 | # keep_warnings = False 145 | 146 | 147 | # -- Options for HTML output ------------------------------------------------- 148 | 149 | # The theme to use for HTML and HTML Help pages. See the documentation for 150 | # a list of builtin themes. 151 | html_theme = "alabaster" 152 | 153 | # Theme options are theme-specific and customize the look and feel of a theme 154 | # further. For a list of options available for each theme, see the 155 | # documentation. 156 | html_theme_options = { 157 | "sidebar_width": "300px", 158 | "page_width": "1200px" 159 | } 160 | 161 | # Add any paths that contain custom themes here, relative to this directory. 162 | # html_theme_path = [] 163 | 164 | # The name for this set of Sphinx documents. If None, it defaults to 165 | # " v documentation". 166 | try: 167 | from ordered import __version__ as version 168 | except ImportError: 169 | pass 170 | else: 171 | release = version 172 | 173 | # A shorter title for the navigation bar. Default is the same as html_title. 174 | # html_short_title = None 175 | 176 | # The name of an image file (relative to this directory) to place at the top 177 | # of the sidebar. 178 | # html_logo = "" 179 | 180 | # The name of an image file (within the static path) to use as favicon of the 181 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 182 | # pixels large. 183 | # html_favicon = None 184 | 185 | # Add any paths that contain custom static files (such as style sheets) here, 186 | # relative to this directory. They are copied after the builtin static files, 187 | # so a file named "default.css" will overwrite the builtin "default.css". 188 | html_static_path = ["_static"] 189 | 190 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 191 | # using the given strftime format. 192 | # html_last_updated_fmt = '%b %d, %Y' 193 | 194 | # If true, SmartyPants will be used to convert quotes and dashes to 195 | # typographically correct entities. 196 | # html_use_smartypants = True 197 | 198 | # Custom sidebar templates, maps document names to template names. 199 | # html_sidebars = {} 200 | 201 | # Additional templates that should be rendered to pages, maps page names to 202 | # template names. 203 | # html_additional_pages = {} 204 | 205 | # If false, no module index is generated. 206 | # html_domain_indices = True 207 | 208 | # If false, no index is generated. 209 | # html_use_index = True 210 | 211 | # If true, the index is split into individual pages for each letter. 212 | # html_split_index = False 213 | 214 | # If true, links to the reST sources are added to the pages. 215 | # html_show_sourcelink = True 216 | 217 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 218 | # html_show_sphinx = True 219 | 220 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 221 | # html_show_copyright = True 222 | 223 | # If true, an OpenSearch description file will be output, and all pages will 224 | # contain a tag referring to it. The value of this option must be the 225 | # base URL from which the finished HTML is served. 226 | # html_use_opensearch = '' 227 | 228 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 229 | # html_file_suffix = None 230 | 231 | # Output file base name for HTML help builder. 232 | htmlhelp_basename = "ordered-doc" 233 | 234 | 235 | # -- Options for LaTeX output ------------------------------------------------ 236 | 237 | latex_elements = { 238 | # The paper size ("letterpaper" or "a4paper"). 239 | # "papersize": "letterpaper", 240 | # The font size ("10pt", "11pt" or "12pt"). 241 | # "pointsize": "10pt", 242 | # Additional stuff for the LaTeX preamble. 243 | # "preamble": "", 244 | } 245 | 246 | # Grouping the document tree into LaTeX files. List of tuples 247 | # (source start file, target name, title, author, documentclass [howto/manual]). 248 | latex_documents = [ 249 | ("index", "user_guide.tex", "ordered Documentation", "Andrew Gree", "manual") 250 | ] 251 | 252 | # The name of an image file (relative to this directory) to place at the top of 253 | # the title page. 254 | # latex_logo = "" 255 | 256 | # For "manual" documents, if this is true, then toplevel headings are parts, 257 | # not chapters. 258 | # latex_use_parts = False 259 | 260 | # If true, show page references after internal links. 261 | # latex_show_pagerefs = False 262 | 263 | # If true, show URL addresses after external links. 264 | # latex_show_urls = False 265 | 266 | # Documents to append as an appendix to all manuals. 267 | # latex_appendices = [] 268 | 269 | # If false, no module index is generated. 270 | # latex_domain_indices = True 271 | 272 | # -- External mapping -------------------------------------------------------- 273 | python_version = ".".join(map(str, sys.version_info[0:2])) 274 | intersphinx_mapping = { 275 | "sphinx": ("http://www.sphinx-doc.org/en/stable", None), 276 | "python": ("https://docs.python.org/" + python_version, None), 277 | "matplotlib": ("https://matplotlib.org", None), 278 | "numpy": ("https://docs.scipy.org/doc/numpy", None), 279 | "sklearn": ("https://scikit-learn.org/stable", None), 280 | "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), 281 | "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), 282 | "pyscaffold": ("https://pyscaffold.org/en/stable", None), 283 | } 284 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | ordered 3 | ======= 4 | 5 | This is the documentation of **ordered**. 6 | 7 | .. note:: 8 | 9 | This is the main page of your project's `Sphinx`_ documentation. 10 | It is formatted in `reStructuredText`_. Add additional pages 11 | by creating rst-files in ``docs`` and adding them to the `toctree`_ below. 12 | Use then `references`_ in order to link them from this page, e.g. 13 | :ref:`authors` and :ref:`changes`. 14 | 15 | It is also possible to refer to the documentation of other Python packages 16 | with the `Python domain syntax`_. By default you can reference the 17 | documentation of `Sphinx`_, `Python`_, `NumPy`_, `SciPy`_, `matplotlib`_, 18 | `Pandas`_, `Scikit-Learn`_. You can add more by extending the 19 | ``intersphinx_mapping`` in your Sphinx's ``conf.py``. 20 | 21 | The pretty useful extension `autodoc`_ is activated by default and lets 22 | you include documentation from docstrings. Docstrings can be written in 23 | `Google style`_ (recommended!), `NumPy style`_ and `classical style`_. 24 | 25 | 26 | Contents 27 | ======== 28 | 29 | .. toctree:: 30 | :maxdepth: 2 31 | 32 | Overview 33 | License 34 | Authors 35 | Changelog 36 | Module Reference 37 | 38 | 39 | Indices and tables 40 | ================== 41 | 42 | * :ref:`genindex` 43 | * :ref:`modindex` 44 | * :ref:`search` 45 | 46 | .. _toctree: http://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html 47 | .. _reStructuredText: http://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html 48 | .. _references: http://www.sphinx-doc.org/en/stable/markup/inline.html 49 | .. _Python domain syntax: http://sphinx-doc.org/domains.html#the-python-domain 50 | .. _Sphinx: http://www.sphinx-doc.org/ 51 | .. _Python: http://docs.python.org/ 52 | .. _Numpy: http://docs.scipy.org/doc/numpy 53 | .. _SciPy: http://docs.scipy.org/doc/scipy/reference/ 54 | .. _matplotlib: https://matplotlib.org/contents.html# 55 | .. _Pandas: http://pandas.pydata.org/pandas-docs/stable 56 | .. _Scikit-Learn: http://scikit-learn.org/stable 57 | .. _autodoc: http://www.sphinx-doc.org/en/stable/ext/autodoc.html 58 | .. _Google style: https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings 59 | .. _NumPy style: https://numpydoc.readthedocs.io/en/latest/format.html 60 | .. _classical style: http://www.sphinx-doc.org/en/stable/domains.html#info-field-lists 61 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. _license: 2 | 3 | ======= 4 | License 5 | ======= 6 | 7 | .. include:: ../LICENSE.txt 8 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. _readme: 2 | .. mdinclude:: ../README.md 3 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements file for ReadTheDocs, check .readthedocs.yml. 2 | # To build the module reference correctly, make sure every external package 3 | # under `install_requires` in `setup.cfg` is also listed here! 4 | sphinx>=3.2.1 5 | # sphinx_rtd_theme 6 | m2r2 7 | -------------------------------------------------------------------------------- /examples/basic_ordered.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append(".") 3 | 4 | import random, ordered 5 | 6 | x = 5 7 | steps = 0 8 | 9 | def plus_x(): 10 | global x, steps 11 | x += 7 12 | steps += 1 13 | 14 | def minus_x(): 15 | global x, steps 16 | x -= 2 17 | steps += 1 18 | 19 | with ordered.orderedcontext(): 20 | # exit ordered context with minimum steps, no exceptions 21 | while x != 22: 22 | random.choice([plus_x, minus_x])() 23 | 24 | print("Steps:", steps) 25 | 26 | -------------------------------------------------------------------------------- /examples/basic_random.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | x = 5 4 | steps = 0 5 | 6 | def plus_x(): 7 | global x, steps 8 | x += 7 9 | steps += 1 10 | 11 | def minus_x(): 12 | global x, steps 13 | x -= 2 14 | steps += 1 15 | 16 | while x != 22: 17 | random.choice([plus_x, minus_x])() 18 | 19 | print("Steps:", steps) 20 | 21 | -------------------------------------------------------------------------------- /ordered/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | __author__ = "Andrew Gree" 4 | __copyright__ = "CriticalHop Inc." 5 | __license__ = "MIT" 6 | 7 | 8 | if sys.version_info[:2] >= (3, 8): 9 | # TODO: Import directly (no need for conditional) when `python_requires = >= 3.8` 10 | from importlib.metadata import PackageNotFoundError, version # pragma: no cover 11 | else: 12 | from importlib_metadata import PackageNotFoundError, version # pragma: no cover 13 | 14 | try: 15 | # Change here if project is renamed and does not equal the package name 16 | dist_name = __name__ 17 | __version__ = version(dist_name) 18 | except PackageNotFoundError: # pragma: no cover 19 | __version__ = "unknown" 20 | finally: 21 | del version, PackageNotFoundError 22 | 23 | from ordered.ordered import orderedcontext, choice, choices 24 | from hyperc import side_effect 25 | -------------------------------------------------------------------------------- /ordered/ordered.py: -------------------------------------------------------------------------------- 1 | __author__ = "Andrew Gree" 2 | __copyright__ = "CriticalHop Inc." 3 | __license__ = "MIT" 4 | 5 | 6 | import builtins 7 | from contextlib import contextmanager 8 | import sys 9 | import tempfile 10 | import types 11 | import inspect 12 | import hyperc 13 | 14 | 15 | _file_cache = {} 16 | _dirty_is_context = False 17 | 18 | 19 | def _get_indent(l): 20 | return len(l) - len(l.lstrip()) 21 | 22 | def _expand_class(local_dict): 23 | # print("Expanding:", list(local_dict.keys())) 24 | for name, x in local_dict.copy().items(): 25 | if name.startswith("_"): 26 | del local_dict[name] 27 | if (not type(x) in vars(builtins).values() 28 | and not x is None 29 | and not isinstance(x, types.FunctionType) 30 | and not isinstance(x, types.ModuleType)): 31 | if x.__class__.__qualname__ in ("SourceFileLoader", "ModuleSpec"): continue 32 | if x.__class__.__name__.startswith("_"): continue 33 | # print("Expanding to class:", repr(x.__class__)) 34 | local_dict[f"{repr(x.__class__)}"] = x.__class__ 35 | return local_dict 36 | 37 | 38 | 39 | def _stub_rewrite_while_choice(code, frame): 40 | """A stub to rewrite while ... choice to assert ... 41 | 42 | Currently `while ..` loop only serves as a "mental model" around HyperC 43 | """ 44 | for ln in range(len(code)): 45 | l = code[ln] 46 | if (l.strip().startswith("while") and 47 | ( 48 | code[ln+1].strip().startswith("ordered.choice") or 49 | code[ln+1].strip().startswith("random.choice") or 50 | code[ln+1].strip().startswith("choice") 51 | )): 52 | # TODO: strip out comments! 53 | if "#" in code[ln]: raise ValueError("Comments not supported on while... line") 54 | code[ln] = l.replace("while", "assert not (").replace(":", "") + ")" 55 | choiceline = code[ln+1] 56 | choice_indent = _get_indent(choiceline) 57 | code[ln+1] = "#"+code[ln+1] 58 | if len(code) > ln+2 and code[ln+2].strip() and _get_indent(code[ln+2]) == choice_indent: 59 | raise ValueError(f"Unsupported while...choice loop complexity: extra `{code[ln+2].strip()}`") 60 | break 61 | else: 62 | raise ValueError("while...ordered.choice() was not found in ordered context block") 63 | 64 | choiceargs = [] 65 | def choice(*args): 66 | def consume(*args): 67 | choiceargs.append(args) 68 | choiceargs.append(args) 69 | return consume 70 | class OrderedStub: 71 | pass 72 | ordered = OrderedStub() 73 | ordered.choice = choice 74 | def get_objects(): 75 | return "GC_OBJECTS" 76 | gc = OrderedStub() 77 | gc.get_objects = get_objects 78 | stub_globals = frame.f_globals.copy() 79 | stub_locals = frame.f_locals.copy() 80 | stub_globals["choice"] = choice 81 | stub_globals["choices"] = choice 82 | stub_globals["ordered"] = ordered 83 | stub_globals["random"] = ordered 84 | stub_globals["gc"] = gc 85 | stub_locals["choice"] = choice 86 | stub_locals["choices"] = choice 87 | stub_locals["ordered"] = ordered 88 | stub_locals["random"] = ordered 89 | stub_locals["gc"] = gc 90 | eval(choiceline, stub_globals, stub_locals) 91 | 92 | # TODO: support situation when choosing with no arguments - from funcitons defined in local context 93 | # the code within the orderedcontext block may include functions to compile 94 | # these functions must be compiled using "normal" flow of program 95 | 96 | if len(choiceargs) < 2 or len(choiceargs) > 3: 97 | raise ValueError("Unsupported ordered.choice invocation") 98 | choiceargs_parsed = {} 99 | if choiceargs[0] and choiceargs[1]: 100 | choiceargs_parsed['functions'] = { 101 | f.__name__:f for f in filter( 102 | lambda x: 103 | isinstance(x, types.FunctionType) or inspect.isclass(x), 104 | choiceargs[0][0])} 105 | choiceargs_parsed['objects'] = choiceargs[1][0] 106 | elif choiceargs[0] and not choiceargs[1]: 107 | choiceargs_parsed['functions'] = { 108 | f.__name__:f for f in filter( 109 | lambda x: 110 | isinstance(x, types.FunctionType) or inspect.isclass(x), 111 | choiceargs[0][0])} 112 | choiceargs_parsed['objects'] = "GC_OBJECTS" 113 | elif not choiceargs[0] and choiceargs[1]: 114 | choiceargs_parsed['functions'] = _expand_class(frame.f_locals) # FIXME: use collected by trace 115 | choiceargs_parsed['objects'] = choiceargs[1][0] 116 | elif not choiceargs[0] and not choiceargs[1]: 117 | choiceargs_parsed['functions'] = _expand_class(frame.f_locals) # FIXME: use collected by trace 118 | choiceargs_parsed['objects'] = "GC_OBJECTS" 119 | else: 120 | raise ValueError("Unsupported choice configuration") 121 | 122 | # print("Choice args parsed:", list(choiceargs_parsed['functions'].keys())) 123 | 124 | return code, choiceargs_parsed 125 | 126 | 127 | def _cached_code(fn): 128 | code = _file_cache.get(fn) 129 | if not code: 130 | code = open(fn).read().split("\n") 131 | _file_cache[fn] = code 132 | return code 133 | 134 | def _scan_to_exitcontext(frame): 135 | "Return code_of_context, exit_lineno" 136 | lineno = frame.f_lineno-1 137 | cur_line = _cached_code(frame.f_code.co_filename)[lineno] 138 | cur_indent = len(cur_line) - len(cur_line.lstrip()) 139 | maxcode = len(_cached_code(frame.f_code.co_filename)) 140 | next_indent = cur_indent 141 | next_line = cur_line 142 | code = [] 143 | while next_indent >= cur_indent: 144 | lineno += 1 145 | code.append(next_line) 146 | if lineno >= maxcode: 147 | break 148 | next_line = _cached_code(frame.f_code.co_filename)[lineno] 149 | next_indent = len(next_line) - len(next_line.lstrip()) 150 | 151 | l_code = [l[cur_indent:] for l in code] 152 | _, choiceargs = _stub_rewrite_while_choice(l_code, frame) 153 | return "\n ".join(l_code), lineno, choiceargs 154 | 155 | def _trace_once(frame, event, arg): 156 | "Jump to line only available inside trace" 157 | # TODO: if line is function definition - run it normally 158 | # and store its name 159 | # to support locally-defined functions 160 | if not frame.f_code.co_name in ("__enter__", "__exit__", "orderedcontext"): 161 | code, lineno, choiceargs = _scan_to_exitcontext(frame) 162 | try: 163 | frame.f_lineno = lineno + 1 # jump to after ordered context 164 | except ValueError as e: 165 | if "comes after" in str(e): 166 | raise RuntimeError(f"Orderedcontext abruptly ends. Please add `pass` statement on line {lineno + 1} of {frame.f_code.co_filename}:{lineno + 1}") 167 | import traceback 168 | traceback.print_exc() 169 | print(f"Likely the orderedcontext abruptly ends. Please add `pass` statement on line {lineno + 1} of {frame.f_code.co_filename}:{lineno + 1}") 170 | raise e 171 | ctx_frame = frame 172 | sys.settrace(None) 173 | while frame: 174 | frame.f_trace = None # not sure 175 | frame = frame.f_back 176 | # Compiling code happens here as there is no way to return to context manager 177 | full_function_code = f"def ordered_ctx_goal():\n "+code 178 | with tempfile.NamedTemporaryFile() as fp: 179 | fp.write(code.encode("utf-8")) 180 | f_code = compile(full_function_code, fp.name, 'exec') 181 | exec(f_code, ctx_frame.f_locals) 182 | func = ctx_frame.f_locals["ordered_ctx_goal"] 183 | hyperc.solve(func, choiceargs["functions"]) 184 | global _dirty_is_context 185 | _dirty_is_context = False 186 | # return _trace 187 | return None 188 | 189 | @contextmanager 190 | def orderedcontext(*args, **kwds): 191 | "Partial context manager." 192 | global _dirty_is_context 193 | _dirty_is_context = True 194 | sys.settrace(_trace_once) 195 | frame = sys._getframe().f_back 196 | while frame: 197 | frame.f_trace = _trace_once 198 | frame = frame.f_back 199 | yield None # Due to code jump it never goes past yield 200 | 201 | def choice(*args): 202 | if not _dirty_is_context: 203 | raise RuntimeError("choice() is only allowed within ordered context") 204 | 205 | def choices(*args): 206 | if not _dirty_is_context: 207 | raise RuntimeError("choices() is only allowed within ordered context") 208 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | # AVOID CHANGING REQUIRES: IT WILL BE UPDATED BY PYSCAFFOLD! 3 | requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=5", "wheel"] 4 | build-backend = "setuptools.build_meta" 5 | 6 | # [tool.setuptools_scm] 7 | # See configuration details in https://github.com/pypa/setuptools_scm 8 | #version_scheme = "no-guess-dev" 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # This file is used to configure your project. 2 | # Read more about the various options under: 3 | # http://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files 4 | 5 | [metadata] 6 | name = ordered 7 | version = 0.0.2 8 | description = Entropy-controlled contexts in Python 9 | author = Andrew Gree 10 | author_email = andrew@hyperc.com 11 | license = MIT 12 | long_description = file: README.md 13 | long_description_content_type = text/markdown; charset=UTF-8 14 | url = https://github.com/hyperc-ai/ordered/ 15 | # Add here related links, for example: 16 | project_urls = 17 | Documentation = https://github.com/hyperc-ai/ordered/ 18 | Source = https://github.com/hyperc-ai/ordered/ 19 | # Changelog = 20 | # Tracker = 21 | # Conda-Forge = 22 | # Download = 23 | # Twitter = https://twitter.com/hyperc_ai 24 | 25 | # Change if running only on Windows, Mac or Linux (comma-separated) 26 | platforms = linux 27 | 28 | # Add here all kinds of additional classifiers as defined under 29 | # https://pypi.python.org/pypi?%3Aaction=list_classifiers 30 | classifiers = 31 | Development Status :: 3 - Alpha 32 | Programming Language :: Python 33 | 34 | 35 | [options] 36 | zip_safe = False 37 | packages = ordered 38 | include_package_data = True 39 | 40 | # Require a min/specific Python version (comma-separated conditions) 41 | python_requires = >=3.8 42 | 43 | # Add here dependencies of your project (line-separated), e.g. requests>=2.2,<3.0. 44 | # Version specifiers like >=2.2,<3.0 avoid problems due to API changes in 45 | # new major versions. This works if the required packages follow Semantic Versioning. 46 | # For more information, check out https://semver.org/. 47 | install_requires = 48 | importlib-metadata 49 | python_version>="3.8" 50 | hyperc 51 | 52 | 53 | [options.packages.find] 54 | where = ordered 55 | exclude = 56 | tests 57 | 58 | [options.extras_require] 59 | # Add here additional requirements for extra features, to install with: 60 | # `pip install ordered[PDF]` like: 61 | # PDF = ReportLab; RXP 62 | 63 | # Add here test requirements (semicolon/line-separated) 64 | testing = 65 | setuptools 66 | pytest 67 | pytest-cov 68 | 69 | [options.entry_points] 70 | # console_scripts = 71 | # script_name = ordered.module:function 72 | 73 | [tool:pytest] 74 | # CAUTION: --cov flags may prohibit setting breakpoints while debugging. 75 | # Comment those flags to avoid this py.test issue. 76 | addopts = 77 | # --cov ordered --cov-report term-missing 78 | # --verbose 79 | norecursedirs = 80 | dist 81 | build 82 | .tox 83 | testpaths = tests 84 | # Use pytest markers to select/deselect specific tests 85 | # markers = 86 | # slow: mark tests as slow (deselect with '-m "not slow"') 87 | # system: mark end-to-end system tests 88 | 89 | [bdist_wheel] 90 | # Use this option if your package is pure-python 91 | universal = 1 92 | 93 | [devpi:upload] 94 | # Options for the devpi: PyPI server and packaging tool 95 | # VCS export must be deactivated since we are using setuptools-scm 96 | no_vcs = 1 97 | formats = bdist_wheel 98 | 99 | [flake8] 100 | # Some sane defaults for the code style checker flake8 101 | max_line_length = 88 102 | extend_ignore = E203, W503 103 | # ^ Black-compatible 104 | # E203 and W503 have edge cases handled by black 105 | exclude = 106 | .tox 107 | build 108 | dist 109 | .eggs 110 | docs/conf.py 111 | 112 | [pyscaffold] 113 | # PyScaffold's parameters when the project was created. 114 | # This will be used when updating. Do not change! 115 | version = 4.0.2 116 | package = ordered 117 | extensions = 118 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Setup file for ordered. 3 | Use setup.cfg to configure your project. 4 | 5 | This file was generated with PyScaffold 4.0.2. 6 | PyScaffold helps you to put up the scaffold of your new Python project. 7 | Learn more under: https://pyscaffold.org/ 8 | """ 9 | from setuptools import setup 10 | 11 | if __name__ == "__main__": 12 | try: 13 | # setup(use_scm_version={"version_scheme": "no-guess-dev"}) 14 | setup() 15 | except: # noqa 16 | print( 17 | "\n\nAn error occurred while building the project, " 18 | "please ensure you have the most updated version of setuptools, " 19 | "setuptools_scm and wheel with:\n" 20 | " pip install -U setuptools setuptools_scm wheel\n\n" 21 | ) 22 | raise 23 | -------------------------------------------------------------------------------- /tests/basic_ordered.py: -------------------------------------------------------------------------------- 1 | import random, ordered 2 | 3 | x = 5 4 | steps = 0 5 | 6 | def plus_x(): 7 | global x, steps 8 | x += 3 9 | steps += 1 10 | 11 | def minus_x(): 12 | global x, steps 13 | x -= 2 14 | steps += 1 15 | 16 | with ordered.orderedcontext(): 17 | # exit ordered context with minimum steps, no exceptions 18 | while x != 12: 19 | random.choice([plus_x, minus_x])() 20 | 21 | print("Steps:", steps) 22 | 23 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read more about conftest.py under: 3 | - https://docs.pytest.org/en/stable/fixture.html 4 | - https://docs.pytest.org/en/stable/writing_plugins.html 5 | """ 6 | 7 | # import pytest 8 | -------------------------------------------------------------------------------- /tests/example_bottle.py: -------------------------------------------------------------------------------- 1 | from ordered import orderedcontext, choice 2 | class Bottle: 3 | volume: int 4 | fill: int 5 | def __init__(self, volume: int): 6 | self.volume = volume 7 | self.fill = 0 8 | def fill_in(self): 9 | self.fill += self.volume 10 | assert self.fill == self.volume 11 | def pour_out(self, bottle: "Bottle"): 12 | assert self != bottle 13 | can_fit = bottle.volume - bottle.fill 14 | sf = self.fill 15 | bf = bottle.fill 16 | if self.fill <= can_fit: 17 | bottle.fill += self.fill 18 | self.fill = 0 19 | assert self.fill == 0 20 | assert bottle.fill == bf + sf 21 | else: 22 | bottle.fill += can_fit 23 | self.fill -= can_fit 24 | assert bottle.fill == bottle.volume 25 | assert self.fill == sf - can_fit 26 | def empty(self): 27 | self.fill = 0 28 | b1 = Bottle(3) 29 | b2 = Bottle(5) 30 | with orderedcontext(): 31 | while b2.fill != 4: 32 | choice([Bottle])() 33 | pass -------------------------------------------------------------------------------- /tests/example_bottle_manual.py: -------------------------------------------------------------------------------- 1 | from ordered import orderedcontext, choice 2 | class Bottle: 3 | volume: int 4 | fill: int 5 | def __init__(self, volume: int): 6 | self.volume = volume 7 | self.fill = 0 8 | def fill_in(self): 9 | self.fill += self.volume 10 | assert self.fill == self.volume 11 | def pour_out(self, bottle: "Bottle"): 12 | assert self != bottle 13 | can_fit = bottle.volume - bottle.fill 14 | sf = self.fill 15 | bf = bottle.fill 16 | if self.fill <= can_fit: 17 | bottle.fill += self.fill 18 | self.fill = 0 19 | assert self.fill == 0 20 | assert bottle.fill == bf + sf 21 | else: 22 | bottle.fill += can_fit 23 | self.fill -= can_fit 24 | assert bottle.fill == bottle.volume 25 | assert self.fill == sf - can_fit 26 | def empty(self): 27 | self.fill = 0 28 | b1 = Bottle(3) 29 | b2 = Bottle(5) 30 | 31 | 32 | b2.fill_in() 33 | b2.pour_out(b1) 34 | b1.empty() 35 | b2.pour_out(b1) 36 | b2.fill_in() 37 | b2.pour_out(b1) 38 | 39 | assert b2.fill == 4 40 | b1 = Bottle(3) 41 | b2 = Bottle(5) 42 | 43 | 44 | b1.fill_in() 45 | b1.pour_out(b2) 46 | b1.fill_in() 47 | b1.pour_out(b2) 48 | b2.empty() 49 | b1.pour_out(b2) 50 | b1.fill_in() 51 | b1.pour_out(b2) 52 | assert b2.fill == 4 -------------------------------------------------------------------------------- /tests/example_oo.py: -------------------------------------------------------------------------------- 1 | import ordered 2 | 3 | class MyVars: 4 | x: int 5 | steps: int 6 | def __init__(self) -> None: 7 | self.x = 0 8 | self.steps = 0 9 | 10 | def plus_x(self): 11 | self.x += 3 12 | self.count_steps() 13 | 14 | def minus_x(self): 15 | self.x -= 2 16 | self.count_steps() 17 | 18 | def count_steps(self): 19 | self.steps += 1 20 | 21 | m = MyVars() 22 | m.x = 5 23 | with ordered.orderedcontext(): 24 | # exit ordered context without exceptions with minimum steps 25 | while m.x != 12: 26 | ordered.choice()() 27 | 28 | print("Steps:", m.steps) -------------------------------------------------------------------------------- /tests/ordered_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append(".") 3 | 4 | import ordered 5 | import gc 6 | 7 | class MyVars: 8 | x: int 9 | def __init__(self) -> None: 10 | self.x = 0 11 | 12 | m = MyVars() 13 | m.x = 5 14 | steps = 0 15 | 16 | def plus_x(m: MyVars): 17 | m.x += 3 18 | global steps 19 | steps += 1 20 | 21 | def minus_x(m: MyVars): 22 | m.x -= 2 23 | global steps 24 | steps += 1 25 | 26 | 27 | with ordered.orderedcontext(): 28 | # exit ordered context without exceptions with minimum steps 29 | while m.x != 12: 30 | ordered.choice([plus_x, minus_x])(ordered.choice(gc.get_objects())) 31 | # hello 32 | 33 | print("Steps:", steps) 34 | 35 | -------------------------------------------------------------------------------- /tests/test_basic.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | sys.path.append("tests") 4 | 5 | __author__ = "Andrew Gree" 6 | __copyright__ = "CriticalHop Inc." 7 | __license__ = "MIT" 8 | 9 | 10 | def test_basic(): 11 | # import tests.ordered_test 12 | import basic_ordered 13 | assert basic_ordered.steps > 1 14 | assert basic_ordered.steps < 5 15 | 16 | 17 | def test_basic_oo(): 18 | # import tests.ordered_test 19 | import ordered_test 20 | assert ordered_test.steps > 1 21 | assert ordered_test.steps < 5 22 | 23 | -------------------------------------------------------------------------------- /tests/test_bottles.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append("tests") 3 | 4 | __author__ = "Andrew Gree" 5 | __copyright__ = "CriticalHop Inc." 6 | __license__ = "MIT" 7 | 8 | 9 | def test_bottle_definition(): 10 | import example_bottle_manual 11 | assert example_bottle_manual.b2.fill == 4 12 | 13 | 14 | def test_bottle_solve(): 15 | import example_bottle 16 | assert example_bottle.b2.fill == 4 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/test_endofframe.py: -------------------------------------------------------------------------------- 1 | __author__ = "Andrew Gree" 2 | __copyright__ = "CriticalHop Inc." 3 | __license__ = "MIT" 4 | 5 | 6 | import ordered 7 | import pytest 8 | 9 | 10 | def frame_end(): 11 | x = 1 12 | with ordered.orderedcontext(): 13 | while x != 1: 14 | ordered.choice()() 15 | 16 | 17 | def frame_end_good(): 18 | x = 1 19 | with ordered.orderedcontext(): 20 | while x != 1: 21 | ordered.choice()() 22 | pass # required due to current Python limitation 23 | 24 | 25 | def test_endofframe(): 26 | "If ordered context's frame abruptly ends with nothing - pass statement is required" 27 | with pytest.raises(RuntimeError): 28 | frame_end() 29 | 30 | def test_endofframe_fixed(): 31 | frame_end_good() 32 | -------------------------------------------------------------------------------- /tests/test_example_oo.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | sys.path.append("tests") 4 | 5 | __author__ = "Andrew Gree" 6 | __copyright__ = "CriticalHop Inc." 7 | __license__ = "MIT" 8 | 9 | 10 | def test_basic(): 11 | # import tests.ordered_test 12 | import example_oo 13 | assert example_oo.m.steps > 1 14 | assert example_oo.m.steps < 5 15 | 16 | -------------------------------------------------------------------------------- /tests/test_global_var.py: -------------------------------------------------------------------------------- 1 | __author__ = "Andrew Gree" 2 | __copyright__ = "CriticalHop Inc." 3 | __license__ = "MIT" 4 | 5 | 6 | import ordered 7 | import pytest 8 | 9 | x = 0 10 | def inc(): 11 | global x 12 | x += 1 13 | 14 | def run_inc(): 15 | global x 16 | with ordered.orderedcontext(): 17 | while x != 1: 18 | ordered.choice()() 19 | pass # required due to current Python limitation 20 | 21 | @pytest.mark.skip("Fix requested at https://github.com/hyperc-ai/ordered/issues/2") 22 | def test_global_access(): 23 | "Setting the breakpoint to line 13 should work" 24 | run_inc() 25 | 26 | -------------------------------------------------------------------------------- /tests/test_partialorder.py: -------------------------------------------------------------------------------- 1 | import pytest, gc 2 | import ordered, random 3 | 4 | __author__ = "Andrew Gree" 5 | __copyright__ = "CriticalHop Inc." 6 | __license__ = "MIT" 7 | 8 | 9 | class MyVars: 10 | x: int 11 | steps: int 12 | def __init__(self) -> None: 13 | self.x = 0 14 | self.steps = 0 15 | 16 | 17 | def test_partial_context_oo(): 18 | m = MyVars() 19 | m.x = 5 20 | 21 | def plus_x(m: MyVars): 22 | m.x += 3 23 | m.steps += 1 24 | 25 | def minus_x(m: MyVars): 26 | m.x -= 2 27 | m.steps += 1 28 | 29 | with ordered.orderedcontext(): 30 | # exit ordered context without exceptions with minimum steps 31 | while m.x != 12: 32 | ordered.choice([plus_x, minus_x])(ordered.choice(gc.get_objects())) 33 | assert m.x == 12 34 | assert m.steps == 4 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/test_partialorder_oo.py: -------------------------------------------------------------------------------- 1 | import pytest, gc 2 | import ordered, random 3 | 4 | __author__ = "Andrew Gree" 5 | __copyright__ = "CriticalHop Inc." 6 | __license__ = "MIT" 7 | 8 | 9 | class MyVars: 10 | x: int 11 | steps: int 12 | def __init__(self) -> None: 13 | self.x = 0 14 | self.steps = 0 15 | 16 | def plus_x(self): 17 | self.x += 3 18 | self.count_steps() 19 | 20 | def minus_x(self): 21 | self.x -= 2 22 | self.count_steps() 23 | 24 | def count_steps(self): 25 | self.steps += 1 26 | 27 | # @pytest.mark.skip(reason="TODO") 28 | def test_partial_context_oo_full(): 29 | m = MyVars() 30 | m.x = 5 31 | with ordered.orderedcontext(): 32 | # exit ordered context without exceptions with minimum steps 33 | while m.x != 12: 34 | ordered.choice([MyVars])() 35 | assert m.x == 12 36 | assert m.steps == 4 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/test_partialorder_oo3.py: -------------------------------------------------------------------------------- 1 | import pytest, gc 2 | import ordered, random 3 | 4 | __author__ = "Andrew Gree" 5 | __copyright__ = "CriticalHop Inc." 6 | __license__ = "MIT" 7 | 8 | 9 | class MyVars: 10 | x: int 11 | steps: int 12 | def __init__(self) -> None: 13 | self.x = 0 14 | self.steps = 0 15 | 16 | def plus_x(self): 17 | self.x += 3 18 | count_steps(self) 19 | 20 | def minus_x(self): 21 | self.x -= 2 22 | count_steps(self) 23 | 24 | def count_steps(o: MyVars): 25 | o.steps += 1 26 | 27 | 28 | # @pytest.mark.skip(reason="TODO") 29 | def test_partial_context_oo_3(): 30 | m = MyVars() 31 | m.x = 5 32 | with ordered.orderedcontext(): 33 | # exit ordered context without exceptions with minimum steps 34 | while m.x != 12: 35 | ordered.choice()() 36 | assert m.x == 12 37 | assert m.steps == 4 38 | 39 | 40 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox configuration file 2 | # Read more under https://tox.readthedocs.org/ 3 | # THIS SCRIPT IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS! 4 | 5 | [tox] 6 | minversion = 3.15 7 | envlist = default 8 | 9 | 10 | [testenv] 11 | description = invoke pytest to run automated tests 12 | isolated_build = True 13 | deps = 14 | pytest 15 | setenv = 16 | TOXINIDIR = {toxinidir} 17 | passenv = 18 | HOME 19 | extras = 20 | testing 21 | commands = 22 | pytest {posargs} 23 | 24 | 25 | [testenv:{clean,build}] 26 | description = 27 | Build (or clean) the package in isolation according to instructions in: 28 | https://setuptools.readthedocs.io/en/latest/build_meta.html#how-to-use-it 29 | https://github.com/pypa/pep517/issues/91 30 | https://github.com/pypa/build 31 | # NOTE: build is still experimental, please refer to the links for updates/issues 32 | skip_install = True 33 | changedir = {toxinidir} 34 | deps = 35 | build: build[virtualenv] 36 | commands = 37 | clean: python -c 'from shutil import rmtree; rmtree("build", True); rmtree("dist", True)' 38 | build: python -m build . 39 | # By default `build` produces wheels, you can also explicitly use the flags `--sdist` and `--wheel` 40 | 41 | 42 | [testenv:{docs,doctests}] 43 | description = invoke sphinx-build to build the docs/run doctests 44 | setenv = 45 | DOCSDIR = {toxinidir}/docs 46 | BUILDDIR = {toxinidir}/docs/_build 47 | docs: BUILD = html 48 | doctests: BUILD = doctest 49 | deps = 50 | -r {toxinidir}/docs/requirements.txt 51 | # ^ requirements.txt shared with Read The Docs 52 | commands = 53 | sphinx-build -b {env:BUILD} -d "{env:BUILDDIR}/doctrees" "{env:DOCSDIR}" "{env:BUILDDIR}/{env:BUILD}" {posargs} 54 | 55 | 56 | [testenv:publish] 57 | description = 58 | Publish the package you have been developing to a package index server. 59 | By default, it uses testpypi. If you really want to publish your package 60 | to be publicly accessible in PyPI, use the `-- --repository pypi` option. 61 | skip_install = True 62 | changedir = {toxinidir} 63 | passenv = 64 | TWINE_USERNAME 65 | TWINE_PASSWORD 66 | TWINE_REPOSITORY 67 | deps = twine 68 | commands = 69 | ; python -m twine check dist/* 70 | python -m twine upload {posargs:--repository pypi} dist/* 71 | --------------------------------------------------------------------------------