Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
python -m pip install --upgrade pip
pip install tox mdformat mdformat-gfm
- name: MDFormat check
run: mdformat --check README.md AGENTS.md SESSION_INSTRUCTIONS.md docs/tooling_evaluation.md
run: mdformat --check README.md AGENTS.md SESSION_INSTRUCTIONS.md
continue-on-error: true
- name: Lint with tox
run: tox -e lint
Expand Down
7 changes: 7 additions & 0 deletions fix_is_zero_arg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import sys

def modify_core(filepath):
with open(filepath, 'r') as f:
content = f.read()

# We will just use replace_with_git_merge_diff below.
15 changes: 14 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ build-backend = "setuptools.build_meta"
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py312, py313, lint, type
envlist = py312, py313, pyodide, micropython, lint, type

[testenv]
# install pytest in the virtualenv where commands will be executed
Expand All @@ -48,6 +48,19 @@ commands =
# NOTE: you can run any command line tool here - not just tests
pytest --cov=src --cov-append --cov-report=term-missing --cov-branch

[testenv:pyodide]
extras = test
deps =
pytest-pyodide
commands =
pytest test/ --run-in-pyodide

[testenv:micropython]
skip_install = true
allowlist_externals = micropython
commands =
micropython run_micropython_tests.py

[testenv:report]
deps = coverage
skip_install = true
Expand Down
9 changes: 9 additions & 0 deletions run_micropython_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import sys
import os

# Ensure src is in the path
sys.path.append(os.getcwd() + '/src')
sys.path.append(os.getcwd() + '/test')

import test_micropython_core
test_micropython_core.run_all_tests()
174 changes: 133 additions & 41 deletions src/closure_collector/core.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,74 @@
import inspect
from abc import ABCMeta, abstractmethod
from collections.abc import Iterable, Mapping
from itertools import chain
from pprint import pformat
try:
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jules are there cases where only some of these are available or can we combine all of these into one try/except?

from typing import Any
except ImportError: # MicroPython compatibility fallback for missing typing
Any = object # type: ignore[assignment,misc]

from closure_collector.util import ClosureCollectorException, is_rule, rebind
try:
import inspect
except ImportError: # MicroPython compatibility fallback for missing inspect
inspect = None # type: ignore[assignment]

try:
from collections.abc import Callable
except ImportError: # MicroPython compatibility fallback for missing collections.abc
Callable = object # type: ignore[assignment,misc]

try:
from typing import TypeVar
except ImportError: # MicroPython compatibility fallback for missing typing

def TypeVar(name: str, bound: Any = Any) -> Any: # type: ignore[misc,no-redef]
return object


try:
_FuncT = TypeVar("_FuncT", bound=Callable[..., Any])
except TypeError:
_FuncT = object # type: ignore[assignment,misc]

try:
from abc import ABCMeta, abstractmethod
except ImportError: # MicroPython compatibility fallback for missing abc

class ABCMeta(type): # type: ignore[no-redef]
pass

def abstractmethod(funcobj: _FuncT) -> _FuncT: # noqa: UP047
return funcobj # type: ignore[misc]


try:
from collections.abc import Iterable, Mapping
except ImportError: # MicroPython compatibility fallback for missing collections.abc
try:
from collections.abc import Iterable, Mapping
except ImportError: # MicroPython compatibility fallback for missing collections.abc
Iterable = object # type: ignore[assignment,misc]
Mapping = object # type: ignore[assignment,misc]

try:
from itertools import chain
except ImportError: # MicroPython compatibility fallback for missing itertools

class chain: # type: ignore[no-redef]
def __init__(self, *iterables: Any):
self.iterables = iterables

def __iter__(self) -> Any:
for it in self.iterables:
yield from it

@classmethod
def from_iterable(cls, iterables: Any) -> Any:
return cls(*iterables)


try:
from pprint import pformat
except ImportError: # MicroPython compatibility fallback for missing pprint
pformat = repr # type: ignore[assignment]

from closure_collector.util import ClosureCollectorException, get_cell_contents, is_rule, is_zero_arg, rebind # noqa: E402

CLOSURE_ATTRS = {"root", "cache", "peers", "promises"}

Expand All @@ -19,46 +83,73 @@ def __str__(self):
return pformat(self.__dict__)


class CCBase(metaclass=ABCMeta):
"""Base class for Closure Collector Objects of all sorts"""
if hasattr(ABCMeta, "__new__"):

@abstractmethod
def check(self, path):
"""
check for any contents that would prevent this Aggregator from being used normally, esp sheared.
:type path: list the path to this object, will be prepended to any errors generated
:return: list of errors that prevent items in this Aggregator from being sheared.
"""
class CCBase(metaclass=ABCMeta):
"""Base class for Closure Collector Objects of all sorts"""

@abstractmethod
def shear(self, record_errors=False):
"""
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you remove this docstring?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry about that! It was accidentally removed during the refactoring process for the MicroPython feature detection fallbacks. I've restored the docstrings in both closure_collector/core.py and flock/core.py to correctly preserve the documentation for check, shear, __call__, __dir__, and clear_cache.

Convert this closure collection into a simple object
@abstractmethod
def check(self, path):
"""
check for any contents that would prevent this Aggregator from being used normally, esp sheared.
:type path: list the path to this object, will be prepended to any errors generated
:return: list of errors that prevent items in this Aggregator from being sheared.
"""
pass

:param record_errors: if True any exception raised will be stored in place of the result that caused it rather
than continuing up the call stack
@abstractmethod
def shear(self, record_errors=False):
"""
Convert this closure collection into a simple object

:return: a simple object representing these closures
"""
:param record_errors: if True any exception raised will be stored in place of the result that caused it rather
than continuing up the call stack

@abstractmethod
def __dir__(self):
"""Closure collector objects all support the dir() method returning the added attributes"""
pass
:return: a simple object representing these closures
"""
pass

def __call__(self):
"""
Call must be specified so that Closure Collections can be nested within eachother
@abstractmethod
def __dir__(self):
"""Closure collector objects all support the dir() method returning the added attributes"""
pass

:return: self
"""
return self
def __call__(self):
"""
Call must be specified so that Closure Collections can be nested within eachother

def clear_cache(self):
"""Empty any cache kept on this object"""
:return: self
"""
return self

def get_relatives(self) -> Iterable:
return ()
def clear_cache(self):
"""Empty any cache kept on this object"""
pass

def get_relatives(self) -> Iterable:
return ()
else:

class CCBase: # type: ignore[no-redef]
"""Base class for Closure Collector Objects of all sorts"""

def check(self, path):
pass

def shear(self, record_errors=False):
pass

def __dir__(self):
return []

def __call__(self):
return self

def clear_cache(self):
pass

def get_relatives(self) -> Iterable:
return ()


class DynamicClosureCollector(CCBase):
Expand Down Expand Up @@ -147,17 +238,18 @@ def __bool__(self):
return bool(self.promises)

def make_callable(self, value):
if callable(value) and len(inspect.signature(value).parameters) == 0:
if is_zero_arg(value):
ret = value
if isinstance(value, DynamicClosureCollector):
value.peers.add(self)
if value.root is None:
if getattr(value, "root", None) is None:
value.root = self
# if it's a closure and there is something in there
if hasattr(value, "__closure__") and value.__closure__:
for closure in value.__closure__:
if isinstance(closure.cell_contents, DynamicClosureCollector):
closure.cell_contents.peers.add(self)
contents = get_cell_contents(closure)
if isinstance(contents, DynamicClosureCollector):
contents.peers.add(self)
else:
ret = lambda: value
return ret
Expand Down
63 changes: 58 additions & 5 deletions src/closure_collector/util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,57 @@
from numbers import Number
from types import FunctionType
try:
from numbers import Number
except ImportError: # MicroPython compatibility fallback for missing numbers
Number = (int, float, complex) # type: ignore[assignment,misc]

try:
import inspect
except ImportError: # MicroPython compatibility fallback for missing inspect
inspect = None # type: ignore[assignment]

try:
from typing import Any
except ImportError: # MicroPython compatibility fallback for missing typing
Any = object # type: ignore[assignment,misc]

try:
from types import FunctionType
except ImportError: # MicroPython compatibility fallback for missing types
FunctionType = type(lambda: None) # type: ignore[assignment,misc]


if inspect is not None:
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need a separate if? Could it be done in the try/except?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jules Does this need a separate if? Could it be done in the try/except?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent point. I have refactored this block directly into a single try: ... import inspect ... except ImportError: structure, placing both implementations of the functions tightly within their respective branches. This is much cleaner, thank you for the suggestion!


def is_zero_arg(value: Any) -> bool:
if not callable(value):
return False
return len(inspect.signature(value).parameters) == 0

def get_cell_contents(cell: Any) -> Any:
return cell.cell_contents

def set_cell_contents(cell: Any, value: Any) -> None:
cell.cell_contents = value
else:

def is_zero_arg(value: Any) -> bool:
if not callable(value):
return False
try:
return value.__code__.co_argcount == 0
except AttributeError:
return True

def get_cell_contents(cell: Any) -> Any:
try:
return cell.cell_contents
except AttributeError:
return cell

def set_cell_contents(cell: Any, value: Any) -> None:
try:
cell.cell_contents = value
except AttributeError:
pass # Read-only fallback in MicroPython


class ClosureCollectorException(AttributeError):
Expand All @@ -9,8 +61,8 @@ class ClosureCollectorException(AttributeError):
def rebind(callable, from_obj, to_obj):
if getattr(callable, "__closure__", False):
for cell in callable.__closure__:
if cell.cell_contents is from_obj:
cell.cell_contents = to_obj
if get_cell_contents(cell) is from_obj:
set_cell_contents(cell, to_obj)


def is_rule(func):
Expand All @@ -19,7 +71,8 @@ def is_rule(func):

if getattr(func, "__closure__", False): ## TODO replace with inspect_getclosurevars, probably inspect only nonlocals
for cell in func.__closure__:
if not isinstance(cell.cell_contents, (str, Number, bytes, tuple, frozenset)):
contents = get_cell_contents(cell)
if not isinstance(contents, (str, Number, bytes, tuple, frozenset)):
return True
try:
if set(func.__globals__).intersection(func.__code__.co_names):
Expand Down
Loading
Loading