Skip to content

Commit

Permalink
Support Python 3.12
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisrink10 committed Dec 27, 2023
1 parent 14af837 commit c09d5f2
Show file tree
Hide file tree
Showing 12 changed files with 117 additions and 61 deletions.
14 changes: 8 additions & 6 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
version: ['3.8', '3.9', '3.10', '3.11']
version: ['3.8', '3.9', '3.10', '3.11', '3.12']
include:
- version: '3.8'
tox-env: py38,py38-mypy,py38-lint,safety
Expand All @@ -25,13 +25,15 @@ jobs:
- version: '3.10'
tox-env: py310,py310-mypy,py310-lint,safety
- version: '3.11'
tox-env: py311,py311-mypy,py311-lint,format,safety
tox-env: py311,py311-mypy,py311-lint,safety
- version: '3.12'
tox-env: py312,py312-mypy,py312-lint,format,safety
- os: windows-latest
version: '3.11'
tox-env: py311,safety
version: '3.12'
tox-env: py312,safety
- os: macos-latest
version: '3.11'
tox-env: py311,safety
version: '3.12'
tox-env: py312,safety
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
* Added rudimentary support for `clojure.stacktrace` with `print-cause-trace` (part of #721)
* Added support for `bytes` literals using a `#b` prefix (#732)
* Added support for Python 3.12 (#734)

### Fixed
* Fix issue with `case` evaluating all of its clauses expressions (#699)
Expand Down
2 changes: 1 addition & 1 deletion docs/pyinterop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ When compiled, a ``kebab-case`` identifier always becomes a ``snake_case`` ident
Python Builtins
---------------

Python features a collection of `builtin <https://docs.python.org/3.8/library/functions.html>`_ functions which are available by default without module qualification in all Python scripts.
Python features a collection of `builtin <https://docs.python.org/3/library/functions.html>`_ functions which are available by default without module qualification in all Python scripts.
Python builtins are available in all Basilisp code as qualified symbols with the ``python`` namespace portion.
It is not required to import anything to enable this functionality.

Expand Down
9 changes: 5 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Software Development :: Compilers",
Expand All @@ -30,23 +31,23 @@ include = ["README.md", "LICENSE"]

[tool.poetry.dependencies]
python = "^3.8"
astor = "^0.8.1"
attrs = ">=20.1.0"
immutables = "^0.15"
immutables = "^0.20"
prompt-toolkit = "^3.0.0"
pyrsistent = "^0.18.0"
python-dateutil = "^2.8.1"
readerwriterlock = "^1.0.8"

pytest = { version = "^6.2.5", optional = true }
astor = { version = "^0.8.1", python = "<3.9", optional = true }
pytest = { version = "^7.0.0", optional = true }
pygments = { version = "^2.9.0", optional = true }

[tool.poetry.dev-dependencies]
black = "*"
docutils = "*"
isort = "*"
pygments = "*"
pytest = "^6.2.5"
pytest = "^7.0.0"
pytest-pycharm = "*"
sphinx = "*"
sphinx-rtd-theme = "*"
Expand Down
52 changes: 37 additions & 15 deletions src/basilisp/contrib/pytest/testrunner.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import importlib.util
import inspect
import sys
import traceback
from pathlib import Path
from types import GeneratorType
from typing import Callable, Iterable, Iterator, Optional, Tuple

import py
import pytest
from _pytest.config import Config
from _pytest.main import Session
from _pytest.nodes import Node

from basilisp import main as basilisp
from basilisp.importer import BasilispImporter
from basilisp.lang import keyword as kw
from basilisp.lang import map as lmap
from basilisp.lang import runtime as runtime
Expand All @@ -26,11 +28,11 @@ def pytest_configure(config):
basilisp.bootstrap("basilisp.test")


def pytest_collect_file(parent, path):
def pytest_collect_file(file_path: Path, path, parent):
"""Primary PyTest hook to identify Basilisp test files."""
if path.ext == ".lpy":
if path.basename.startswith("test_") or path.purebasename.endswith("_test"):
return BasilispFile.from_parent(parent, fspath=path)
if file_path.suffix == ".lpy":
if file_path.name.startswith("test_") or file_path.name.endswith("_test"):
return BasilispFile.from_parent(parent, fspath=path, path=file_path)
return None


Expand Down Expand Up @@ -140,13 +142,21 @@ class BasilispFile(pytest.File):

def __init__( # pylint: disable=too-many-arguments
self,
fspath: py.path.local,
parent=None,
config: Optional[Config] = None,
session: Optional["Session"] = None,
path: Path,
name: Optional[str] = None,
parent: Optional[Node] = None,
config: Optional[pytest.Config] = None,
session: Optional[pytest.Session] = None,
nodeid: Optional[str] = None,
) -> None:
super().__init__(fspath, parent, config, session, nodeid)
super().__init__(
path=path,
name=name,
parent=parent,
config=config,
session=session,
nodeid=nodeid,
)
self._fixture_manager: Optional[FixtureManager] = None

@staticmethod
Expand Down Expand Up @@ -192,16 +202,28 @@ def teardown(self) -> None:
assert self._fixture_manager is not None
self._fixture_manager.teardown()

@property
def _importer(self) -> BasilispImporter:
return next(l for l in sys.meta_path if isinstance(l, BasilispImporter))

def _import_module(self) -> runtime.BasilispModule:
spec = self._importer.find_spec(self.path.stem, [str(self.path.parent)])
if spec is None:
raise ImportError(f"Failed to import test module '{self.name}'")
module = importlib.util.module_from_spec(spec)
assert isinstance(module, runtime.BasilispModule)
spec.loader.exec_module(module)
return module

def collect(self):
"""Collect all of the tests in the namespace (module) given.
Basilisp's test runner imports the namespace which will (as a side effect)
collect all of the test functions in a namespace (represented by `deftest`
forms in Basilisp). BasilispFile.collect fetches those test functions and
generates BasilispTestItems for PyTest to run the tests."""
filename = self.fspath.basename
module = self.fspath.pyimport()
assert isinstance(module, runtime.BasilispModule)
filename = self.path.name
module = self._import_module()
ns = module.__basilisp_namespace__
once_fixtures, each_fixtures = self._collected_fixtures(ns)
self._fixture_manager = FixtureManager(once_fixtures)
Expand Down
34 changes: 28 additions & 6 deletions src/basilisp/lang/compiler/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import itertools
import os
import sys
import types
from typing import Any, Callable, Iterable, List, Optional

from astor import code_gen as codegen

from basilisp import _pyast as ast
from basilisp.lang import map as lmap
from basilisp.lang import runtime as runtime
Expand Down Expand Up @@ -37,10 +36,33 @@
_DEFAULT_FN = "__lisp_expr__"


def to_py_str(t: ast.AST) -> str:
"""Return a string of the Python code which would generate the input
AST node."""
return codegen.to_source(t)
if sys.version_info >= (3, 9):
from ast import unparse

def to_py_str(t: ast.AST) -> str:
"""Return a string of the Python code which would generate the input
AST node."""
return unparse(t) + "\n\n"

else:
try:
from astor import code_gen as codegen

def to_py_str(t: ast.AST) -> str:
"""Return a string of the Python code which would generate the input
AST node."""
return codegen.to_source(t)

except ImportError:
import warnings

def to_py_str(t: ast.AST) -> str: # pylint: disable=unused-arg
warnings.warn(
"Unable to generate Python code from generated AST due to missing "
"dependency 'astor'",
RuntimeWarning,
)
return ""


BytecodeCollector = Callable[[types.CodeType], None]
Expand Down
13 changes: 4 additions & 9 deletions src/basilisp/lang/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Callable, Iterable, Mapping, Optional, Tuple, TypeVar, Union, cast

from immutables import Map as _Map
from immutables import MapMutation

from basilisp.lang.interfaces import (
IEvolveableCollection,
Expand All @@ -18,12 +19,6 @@
from basilisp.lang.vector import MapEntry
from basilisp.util import partition

try:
from immutables._map import MapMutation
except ImportError:
from immutables.map import MapMutation # type: ignore[assignment]


T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")
Expand Down Expand Up @@ -208,7 +203,7 @@ def update(self, *maps: Mapping[K, V]) -> "PersistentMap":
m: _Map = self._inner.update(*(m.items() for m in maps))
return PersistentMap(m)

def update_with(
def update_with( # type: ignore[return]
self, merge_fn: Callable[[V, V], V], *maps: Mapping[K, V]
) -> "PersistentMap[K, V]":
with self._inner.mutate() as m:
Expand All @@ -217,7 +212,7 @@ def update_with(
m.set(k, merge_fn(m[k], v) if k in m else v)
return PersistentMap(m.finish())

def cons( # type: ignore[override]
def cons( # type: ignore[override, return]
self,
*elems: Union[
IPersistentMap[K, V],
Expand Down Expand Up @@ -279,7 +274,7 @@ def m(**kvs) -> PersistentMap[str, V]:
return PersistentMap.from_coll(kvs)


def from_entries(entries: Iterable[MapEntry[K, V]]) -> PersistentMap[K, V]:
def from_entries(entries: Iterable[MapEntry[K, V]]) -> PersistentMap[K, V]: # type: ignore[return]
with _Map().mutate() as m: # type: ignore[var-annotated]
for entry in entries:
m.set(entry.key, entry.value)
Expand Down
2 changes: 1 addition & 1 deletion src/basilisp/lang/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
NS_VAR_SYM = sym.symbol(NS_VAR_NAME, ns=CORE_NS)
NS_VAR_NS = CORE_NS
REPL_DEFAULT_NS = "basilisp.user"
SUPPORTED_PYTHON_VERSIONS = frozenset({(3, 8), (3, 9), (3, 10), (3, 11)})
SUPPORTED_PYTHON_VERSIONS = frozenset({(3, 8), (3, 9), (3, 10), (3, 11), (3, 12)})

# Public basilisp.core symbol names
COMPILER_OPTIONS_VAR_NAME = "*compiler-options*"
Expand Down
11 changes: 3 additions & 8 deletions src/basilisp/lang/set.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import AbstractSet, Iterable, Optional, TypeVar

from immutables import Map as _Map
from immutables import MapMutation

from basilisp.lang.interfaces import (
IEvolveableCollection,
Expand All @@ -15,12 +16,6 @@
from basilisp.lang.obj import seq_lrepr as _seq_lrepr
from basilisp.lang.seq import sequence

try:
from immutables._map import MapMutation
except ImportError:
from immutables.map import MapMutation # type: ignore[assignment]


T = TypeVar("T")


Expand Down Expand Up @@ -153,13 +148,13 @@ def meta(self) -> Optional[IPersistentMap]:
def with_meta(self, meta: Optional[IPersistentMap]) -> "PersistentSet[T]":
return set(self._inner, meta=meta)

def cons(self, *elems: T) -> "PersistentSet[T]":
def cons(self, *elems: T) -> "PersistentSet[T]": # type: ignore[return]
with self._inner.mutate() as m:
for elem in elems:
m.set(elem, elem)
return PersistentSet(m.finish(), meta=self.meta)

def disj(self, *elems: T) -> "PersistentSet[T]":
def disj(self, *elems: T) -> "PersistentSet[T]": # type: ignore[return]
with self._inner.mutate() as m:
for elem in elems:
try:
Expand Down
10 changes: 4 additions & 6 deletions tests/basilisp/importer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
from unittest.mock import patch

import pytest
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import Testdir

from basilisp import importer as importer
from basilisp.lang import runtime as runtime
Expand All @@ -35,7 +33,7 @@ def test_hook_imports():
assert 1 == importer_counter()


def test_demunged_import(testdir: Testdir):
def test_demunged_import(pytester: pytest.Pytester):
with TemporaryDirectory() as tmpdir:
tmp_module = os.path.join(
tmpdir, "long__AMP__namespace_name__PLUS__with___LT__punctuation__GT__.lpy"
Expand Down Expand Up @@ -85,19 +83,19 @@ def _ns_and_module(filename: str) -> Tuple[str, str]:

class TestImporter:
@pytest.fixture
def do_cache_namespaces(self, monkeypatch):
def do_cache_namespaces(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv(importer._NO_CACHE_ENVVAR, "false")

@pytest.fixture
def do_not_cache_namespaces(self, monkeypatch):
def do_not_cache_namespaces(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv(importer._NO_CACHE_ENVVAR, "true")

@pytest.fixture
def module_cache(self):
return {name: module for name, module in sys.modules.items()}

@pytest.fixture
def module_dir(self, monkeypatch: MonkeyPatch, module_cache):
def module_dir(self, monkeypatch: pytest.MonkeyPatch, module_cache):
with TemporaryDirectory() as tmpdir:
cwd = os.getcwd()
monkeypatch.chdir(tmpdir)
Expand Down
Loading

0 comments on commit c09d5f2

Please sign in to comment.