Skip to content

Commit

Permalink
feat: Add support Python 3.7
Browse files Browse the repository at this point in the history
  • Loading branch information
pawamoy committed Dec 20, 2021
1 parent 4d62c4d commit 4535adc
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 61 deletions.
3 changes: 2 additions & 1 deletion config/mypy.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[mypy]
ignore_missing_imports = true
exclude = tests/fixtures/
warn_unused_ignores = true
# TODO: set to true once Python 3.7 is dropped
warn_unused_ignores = false
show_error_codes = true
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ to generate API documentation or find breaking changes in your API."""
authors = [{name = "Timothée Mazzucotelli", email = "pawamoy@pm.me"}]
license = {file = "LICENSE"}
readme = "README.md"
requires-python = ">=3.8"
requires-python = ">=3.7"
keywords = []
dynamic = ["version"]
classifiers = [
Expand All @@ -20,6 +20,7 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
Expand All @@ -30,7 +31,9 @@ classifiers = [
"Topic :: Utilities",
"Typing :: Typed",
]
dependencies = []
dependencies = [
"cached_property; python_version < '3.8'",
]

[project.optional-dependencies]
async = [
Expand Down
2 changes: 1 addition & 1 deletion scripts/multirun.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -e

PYTHON_VERSIONS="${PYTHON_VERSIONS-3.8 3.9 3.10 3.11}"
PYTHON_VERSIONS="${PYTHON_VERSIONS-3.7 3.8 3.9 3.10 3.11}"

if [ -n "${PYTHON_VERSIONS}" ]; then
for python_version in ${PYTHON_VERSIONS}; do
Expand Down
2 changes: 1 addition & 1 deletion scripts/setup.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -e

PYTHON_VERSIONS="${PYTHON_VERSIONS-3.8 3.9 3.10 3.11}"
PYTHON_VERSIONS="${PYTHON_VERSIONS-3.7 3.8 3.9 3.10 3.11}"
DEPGROUPS="-G async"

install_with_pipx() {
Expand Down
68 changes: 55 additions & 13 deletions src/griffe/agents/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from ast import Name as NodeName
from ast import Not as NodeNot
from ast import NotEq as NodeNotEq
from ast import Num as NodeNum
from ast import Or as NodeOr
from ast import Set as NodeSet
from ast import Slice as NodeSlice
Expand All @@ -50,16 +51,26 @@
from ast import arguments as NodeArguments
from ast import comprehension as NodeComprehension
from ast import keyword as NodeKeyword
from functools import cached_property, partial
from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Sequence, Type

from griffe.collections import LinesCollection
from griffe.exceptions import LastNodeError, RootNodeError
from griffe.expressions import Expression, Name

# TODO: remove once Python 3.7 support is dropped
if sys.version_info < (3, 8):
from ast import NameConstant as NodeNameConstant

from cached_property import cached_property
else:
from functools import cached_property # noqa: WPS440

# TODO: remove once Python 3.8 support is dropped
if sys.version_info < (3, 9):
from ast import Index as NodeIndex

if TYPE_CHECKING:
from griffe.dataclasses import Class, Module

Expand All @@ -69,6 +80,10 @@ class ASTNode:

parent: ASTNode

# TODO: remove once Python 3.7 support is dropped
if sys.version_info < (3, 8): # noqa: WPS604
end_lineno = property(lambda node: None)

@cached_property
def kind(self) -> str:
"""Return the kind of this node.
Expand All @@ -78,7 +93,7 @@ def kind(self) -> str:
"""
return self.__class__.__name__.lower()

@cached_property
@cached_property # noqa: WPS231
def children(self) -> Sequence[ASTNode]: # noqa: WPS231
"""Build and return the children of this node.
Expand Down Expand Up @@ -162,7 +177,7 @@ def previous(self) -> ASTNode:
except IndexError as error:
raise LastNodeError("there is no previous node") from error

@cached_property
@cached_property # noqa: A003
def next(self) -> ASTNode: # noqa: A003
"""Return the next sibling of this node.
Expand Down Expand Up @@ -535,12 +550,6 @@ def _get_subscript_annotation(node: NodeSubscript, parent: Module | Class) -> Ex
return Expression(left, "[", subscript, "]")


if sys.version_info < (3, 9):

def _get_index_annotation(node: NodeIndex, parent: Module | Class) -> str | Name | Expression:
return _get_annotation(node.value, parent)


def _get_tuple_annotation(node: NodeTuple, parent: Module | Class) -> Expression:
return Expression(*_join([_get_annotation(el, parent) for el in node.elts], ", "))

Expand All @@ -561,9 +570,24 @@ def _get_list_annotation(node: NodeList, parent: Module | Class) -> Expression:
NodeList: _get_list_annotation,
}

# TODO: remove once Python 3.8 support is dropped
if sys.version_info < (3, 9):

def _get_index_annotation(node: NodeIndex, parent: Module | Class) -> str | Name | Expression:
return _get_annotation(node.value, parent)

_node_annotation_map[NodeIndex] = _get_index_annotation

# TODO: remove once Python 3.7 support is dropped
if sys.version_info < (3, 8):

def _get_nameconstant_annotation(node: NodeNameConstant, parent: Module | Class) -> str | Name | Expression:
if node.value is None:
return repr(None)
return _get_annotation(node.value, parent)

_node_annotation_map[NodeNameConstant] = _get_nameconstant_annotation


def _get_annotation(node: AST, parent: Module | Class) -> str | Name | Expression:
return _node_annotation_map[type(node)](node, parent)
Expand Down Expand Up @@ -607,9 +631,9 @@ def get_docstring(
else:
return None, None, None
if isinstance(doc, NodeConstant) and isinstance(doc.value, str):
return doc.value, doc.lineno, doc.end_lineno
return doc.value, doc.lineno, doc.end_lineno # type: ignore[attr-defined]
if isinstance(doc, NodeStr):
return doc.s, doc.lineno, doc.end_lineno
return doc.s, doc.lineno, doc.end_lineno # type: ignore[attr-defined]
return None, None, None


Expand Down Expand Up @@ -847,6 +871,7 @@ def _get_call_value(node: NodeCall) -> str:
NodeDiv: _get_div_value,
}

# TODO: remove once Python 3.8 support is dropped
if sys.version_info < (3, 9):

def _get_index_value(node: NodeIndex) -> str:
Expand All @@ -855,6 +880,23 @@ def _get_index_value(node: NodeIndex) -> str:
_node_value_map[NodeIndex] = _get_index_value


# TODO: remove once Python 3.7 support is dropped
if sys.version_info < (3, 8):

def _get_str_value(node: NodeStr) -> str:
return repr(node.s)

def _get_nameconstant_value(node: NodeNameConstant) -> str:
return repr(node.value)

def _get_num_value(node: NodeNum) -> str:
return repr(node.n)

_node_value_map[NodeStr] = _get_str_value
_node_value_map[NodeNameConstant] = _get_nameconstant_value
_node_value_map[NodeNum] = _get_num_value


def get_value(node: AST) -> str:
"""Extract a complex value as a string.
Expand Down Expand Up @@ -955,7 +997,7 @@ def get_parameter_default(node: AST, filepath: Path, lines_collection: LinesColl
return repr(node.value)
if isinstance(node, NodeName):
return node.id
if node.lineno == node.end_lineno:
return lines_collection[filepath][node.lineno - 1][node.col_offset : node.end_col_offset]
if node.lineno == node.end_lineno: # type: ignore[attr-defined]
return lines_collection[filepath][node.lineno - 1][node.col_offset : node.end_col_offset] # type: ignore[attr-defined]
# TODO: handle multiple line defaults
return None
36 changes: 28 additions & 8 deletions src/griffe/agents/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ def visit_classdef(self, node: ast.ClassDef) -> None:
lineno = node.decorator_list[0].lineno
self.in_decorator = True
for decorator_node in node.decorator_list:
decorators.append(Decorator(decorator_node.lineno, decorator_node.end_lineno))
decorators.append(Decorator(decorator_node.lineno, decorator_node.end_lineno)) # type: ignore[attr-defined]
self.visit(decorator_node)
self.in_decorator = False
else:
Expand All @@ -224,7 +224,7 @@ def visit_classdef(self, node: ast.ClassDef) -> None:
class_ = Class(
name=node.name,
lineno=lineno,
endlineno=node.end_lineno,
endlineno=node.end_lineno, # type: ignore[attr-defined]
docstring=self._get_docstring(node),
decorators=decorators,
bases=bases,
Expand All @@ -249,7 +249,7 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
lineno = node.decorator_list[0].lineno
self.in_decorator = True
for decorator_node in node.decorator_list:
decorators.append(Decorator(decorator_node.lineno, decorator_node.end_lineno))
decorators.append(Decorator(decorator_node.lineno, decorator_node.end_lineno)) # type: ignore[attr-defined]
self.visit(decorator_node)
self.in_decorator = False
else:
Expand All @@ -259,13 +259,23 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
parameters = Parameters()
annotation: str | Name | Expression | None

# TODO: remove once Python 3.7 support is dropped
try:
posonlyargs = node.args.posonlyargs # type: ignore[attr-defined]
except AttributeError:
posonlyargs = []

# TODO: probably some optimizations to do here
args_kinds_defaults = reversed(
(
*zip_longest( # noqa: WPS356
reversed(
(
*zip_longest(node.args.posonlyargs, [], fillvalue=ParameterKind.positional_only),
*zip_longest(
posonlyargs, # type: ignore[attr-defined]
[],
fillvalue=ParameterKind.positional_only,
),
*zip_longest(node.args.args, [], fillvalue=ParameterKind.positional_or_keyword),
),
),
Expand Down Expand Up @@ -321,7 +331,7 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
function = Function(
name=node.name,
lineno=lineno,
endlineno=node.end_lineno,
endlineno=node.end_lineno, # type: ignore[union-attr]
parameters=parameters,
returns=get_annotation(node.returns, parent=self.current),
decorators=decorators,
Expand Down Expand Up @@ -362,7 +372,12 @@ def visit_import(self, node: ast.Import) -> None:
alias_path = name.name.split(".", 1)[0]
alias_name = name.asname or alias_path
self.current.imports[alias_name] = alias_path
self.current[alias_name] = Alias(alias_name, alias_path, lineno=node.lineno, endlineno=node.end_lineno)
self.current[alias_name] = Alias(
alias_name,
alias_path,
lineno=node.lineno,
endlineno=node.end_lineno, # type: ignore[attr-defined]
)
self.generic_visit(node)

def visit_importfrom(self, node: ast.ImportFrom) -> None:
Expand All @@ -375,7 +390,12 @@ def visit_importfrom(self, node: ast.ImportFrom) -> None:
alias_name = name.asname or name.name
alias_path = f"{node.module}.{name.name}"
self.current.imports[name.asname or name.name] = alias_path
self.current[alias_name] = Alias(alias_name, alias_path, lineno=node.lineno, endlineno=node.end_lineno)
self.current[alias_name] = Alias(
alias_name,
alias_path,
lineno=node.lineno,
endlineno=node.end_lineno, # type: ignore[attr-defined]
)
self.generic_visit(node)

def handle_attribute( # noqa: WPS231
Expand Down Expand Up @@ -441,7 +461,7 @@ def handle_attribute( # noqa: WPS231
value=value,
annotation=annotation,
lineno=node.lineno,
endlineno=node.end_lineno,
endlineno=node.end_lineno, # type: ignore[union-attr]
docstring=docstring,
)
attribute.labels |= labels
Expand Down
6 changes: 2 additions & 4 deletions src/griffe/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,7 @@ def _load_packages(


def get_parser() -> argparse.ArgumentParser:
"""
Return the program argument parser.
"""Return the program argument parser.
Returns:
The argument parser for the program.
Expand Down Expand Up @@ -162,8 +161,7 @@ def get_parser() -> argparse.ArgumentParser:


def main(args: list[str] | None = None) -> int: # noqa: WPS231
"""
Run the main program.
"""Run the main program.
This function is executed when you type `griffe` or `python -m griffe`.
Expand Down
35 changes: 34 additions & 1 deletion src/griffe/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

from __future__ import annotations

import tokenize
from collections import defaultdict
from functools import lru_cache
from io import BytesIO
from pathlib import Path
from typing import TYPE_CHECKING, Any

from griffe.mixins import GetMembersMixin, SetCollectionMembersMixin
Expand All @@ -10,12 +15,40 @@
from griffe.dataclasses import Module


class LinesCollection(dict): # noqa: WPS600
class LinesCollection:
"""A simple dictionary containing the modules source code lines."""

def __init__(self) -> None:
"""Initialize the collection."""
self._data: dict[Path, list[str]] = {}

def __getitem__(self, key: Path) -> list[str]:
return self._data[key]

def __setitem__(self, key: Path, value: list[str]) -> None:
self._data[key] = value

def __bool__(self):
return True

# TODO: remove once Python 3.7 support is dropped
@lru_cache(maxsize=None)
def tokens(self, path: Path) -> tuple[list[tokenize.TokenInfo], defaultdict]:
"""Tokenize the code.
Parameters:
path: The filepath to get the tokens of.
Returns:
A token list and a mapping of tokens by line number.
"""
readline = BytesIO("\n".join(self[path]).encode()).readline
tokens = list(tokenize.tokenize(readline))
token_table = defaultdict(list) # mapping line numbers to token numbers
for index, token in enumerate(tokens):
token_table[token.start[0]].append(index)
return tokens, token_table


class ModulesCollection(GetMembersMixin, SetCollectionMembersMixin):
"""A collection of modules, allowing easy access to members."""
Expand Down
Loading

0 comments on commit 4535adc

Please sign in to comment.