Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ jobs:
os:
- ubuntu-latest
python:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- "3.13"
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
Expand Down
34 changes: 18 additions & 16 deletions DEVELOPMENT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Environment setup
-----------------
### 1. Install Python versions

Our officially supported Python versions are 3.8, 3.9 and 3.10.
Our officially supported Python versions are 3.10, 3.11, 3.12, 3.13 and 3.14.
Our CI/CD pipeline is setup to run unit tests against Python 3 versions. Make sure you test it before sending a Pull Request.
See [Unit testing with multiple Python versions](#unit-testing-with-multiple-python-versions).

Expand All @@ -40,11 +40,13 @@ easily setup multiple Python versions. For
1. Install PyEnv -
`curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash`
1. Restart shell so the path changes take effect - `exec $SHELL`
1. `pyenv install 3.8.16`
1. `pyenv install 3.9.16`
1. `pyenv install 3.10.9`
1. `pyenv install 3.10.20`
1. `pyenv install 3.11.15`
1. `pyenv install 3.12.13`
1. `pyenv install 3.13.12`
1. `pyenv install 3.14.3`
3. Make Python versions available in the project:
`pyenv local 3.8.16 3.9.16 3.10.9`
`pyenv local 3.10.20 3.11.15 3.12.13 3.13.12 3.14.3`

Note: also make sure the following lines were written into your `.bashrc` (or `.zshrc`, depending on which shell you are using):
```
Expand All @@ -65,7 +67,7 @@ can be found [here](https://black.readthedocs.io/en/stable/integrations/editors.
Since black is installed in virtualenv, when you follow [this instruction](https://black.readthedocs.io/en/stable/integrations/editors.html), `which black` might give you this

```bash
(sam38) $ where black
(sam310) $ where black
/Users/<username>/.pyenv/shims/black
```

Expand All @@ -76,11 +78,11 @@ and this will happen:
pyenv: black: command not found

The `black' command exists in these Python versions:
3.8.16/envs/sam38
sam38
3.10.16/envs/sam310
sam310
```

A simple workaround is to use `/Users/<username>/.pyenv/versions/sam38/bin/black`
A simple workaround is to use `/Users/<username>/.pyenv/versions/sam310/bin/black`
instead of `/Users/<username>/.pyenv/shims/black`.

#### Pre-commit
Expand All @@ -98,15 +100,15 @@ handy plugin that can create virtualenv.
Depending on the python version, the following commands would change to
be the appropriate python version.

1. Create Virtualenv `sam38` for Python3.8: `pyenv virtualenv 3.8.16 sam38`
1. Activate Virtualenv: `pyenv activate sam38`
1. Create Virtualenv `sam310` for Python3.10: `pyenv virtualenv 3.10.16 sam310`
1. Activate Virtualenv: `pyenv activate sam310`

### 4. Install dev version of SAM transform

We will install a development version of SAM transform from source into the
virtualenv.

1. Activate Virtualenv: `pyenv activate sam38`
1. Activate Virtualenv: `pyenv activate sam310`
1. Install dev version of SAM transform: `make init`

Running tests
Expand All @@ -120,10 +122,10 @@ Run `make test` or `make test-fast`. Once all tests pass make sure to run

### Unit testing with multiple Python versions

Currently, our officially supported Python versions are 3.8, 3.9 and 3.10. For the most
part, code that works in Python3.8 will work in Pythons 3.9 and 3.10. You only run into problems if you are
trying to use features released in a higher version (for example features introduced into Python3.10
will not work in Python3.9). If you want to test in many versions, you can create a virtualenv for
Currently, our officially supported Python versions are 3.10, 3.11, 3.12, 3.13 and 3.14. For the most
part, code that works in Python 3.10 will work in later versions. You only run into problems if you are
trying to use features released in a higher version (for example features introduced into Python 3.13
will not work in Python 3.12). If you want to test in many versions, you can create a virtualenv for
each version and flip between them (sourcing the activate script). Typically, we run all tests in
one python version locally and then have our ci (appveyor) run all supported versions.

Expand Down
3 changes: 1 addition & 2 deletions bin/_file_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import sys
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Type


class FileFormatter(ABC):
Expand Down Expand Up @@ -34,7 +33,7 @@ def format_str(self, input_str: str) -> str:

@staticmethod
@abstractmethod
def decode_exception() -> Type[Exception]:
def decode_exception() -> type[Exception]:
"""Return the exception class when the file content cannot be decoded."""

@staticmethod
Expand Down
14 changes: 8 additions & 6 deletions bin/add_transform_test.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
#!/usr/bin/env python
"""Automatically create transform tests input and output files given an input template."""

import argparse
import json
import shutil
import subprocess
import sys
from copy import deepcopy
from pathlib import Path
from typing import Any, Dict
from typing import Any
from unittest.mock import patch

import boto3
Expand All @@ -31,12 +32,12 @@
CLI_OPTIONS = parser.parse_args()


def read_json_file(file_path: Path) -> Dict[str, Any]:
template: Dict[str, Any] = json.loads(file_path.read_text(encoding="utf-8"))
def read_json_file(file_path: Path) -> dict[str, Any]:
template: dict[str, Any] = json.loads(file_path.read_text(encoding="utf-8"))
return template


def write_json_file(obj: Dict[str, Any], file_path: Path) -> None:
def write_json_file(obj: dict[str, Any], file_path: Path) -> None:
with file_path.open("w", encoding="utf-8") as f:
json.dump(obj, f, indent=2, sort_keys=True)

Expand All @@ -54,8 +55,9 @@ def generate_transform_test_output_files(input_file_path: Path, file_basename: s
}

for _, (region, output_path) in transform_test_output_paths.items():
with patch("samtranslator.translator.arn_generator._get_region_from_session", return_value=region), patch(
"boto3.session.Session.region_name", region
with (
patch("samtranslator.translator.arn_generator._get_region_from_session", return_value=region),
patch("boto3.session.Session.region_name", region),
):
# Implicit API Plugin may alter input template file, thus passing a copy here.
output_fragment = transform(deepcopy(manifest), {}, ManagedPolicyLoader(iam_client))
Expand Down
4 changes: 2 additions & 2 deletions bin/json-format.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
#!/usr/bin/env python
"""JSON file formatter (without prettier)."""

import sys
from pathlib import Path

# To allow this script to be executed from other directories
sys.path.insert(0, str(Path(__file__).absolute().parent.parent))

import json
from typing import Type

from bin._file_formatter import FileFormatter

Expand All @@ -23,7 +23,7 @@ def format_str(self, input_str: str) -> str:
return json.dumps(obj, indent=2, sort_keys=True) + "\n"

@staticmethod
def decode_exception() -> Type[Exception]:
def decode_exception() -> type[Exception]:
return json.JSONDecodeError

@staticmethod
Expand Down
4 changes: 2 additions & 2 deletions bin/parse_cdk_cfn_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@

import json
import sys
from typing import Any, Dict
from typing import Any


def main() -> None:
obj = json.load(sys.stdin)

out: Dict[str, Any] = {"properties": {}}
out: dict[str, Any] = {"properties": {}}
for k, v in obj["Types"].items():
kk = k.replace(".", " ")
vv = v["properties"]
Expand Down
45 changes: 23 additions & 22 deletions bin/public_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
(see https://peps.python.org/pep-0008/#descriptive-naming-styles)
This CLI tool helps automate the detection of compatibility-breaking changes.
"""

import argparse
import ast
import importlib
Expand All @@ -17,17 +18,17 @@
import string
import sys
from pathlib import Path
from typing import Any, Dict, List, NamedTuple, Optional, Set, Union
from typing import Any, NamedTuple, Union

_ARGUMENT_SELF = {"kind": "POSITIONAL_OR_KEYWORD", "name": "self"}
_PRINTABLE_CHARS = set(string.printable)


class InterfaceScanner:
def __init__(self, skipped_modules: Optional[List[str]] = None) -> None:
self.signatures: Dict[str, Union[inspect.Signature]] = {}
self.variables: Set[str] = set()
self.skipped_modules: Set[str] = set(skipped_modules or [])
def __init__(self, skipped_modules: list[str] | None = None) -> None:
self.signatures: dict[str, Union[inspect.Signature]] = {}
self.variables: set[str] = set()
self.skipped_modules: set[str] = set(skipped_modules or [])

def scan_interfaces_recursively(self, module_name: str) -> None:
if module_name in self.skipped_modules:
Expand Down Expand Up @@ -63,7 +64,7 @@ def _scan_variables_in_module(self, module_name: str) -> None:
else:
module_path = module_path.with_suffix(".py")
tree = ast.parse("".join([char for char in module_path.read_text() if char in _PRINTABLE_CHARS]))
assignments: List[ast.Assign] = [node for node in ast.iter_child_nodes(tree) if isinstance(node, ast.Assign)]
assignments: list[ast.Assign] = [node for node in ast.iter_child_nodes(tree) if isinstance(node, ast.Assign)]
for assignment in assignments:
for target in assignment.targets:
if not isinstance(target, ast.Name):
Expand Down Expand Up @@ -97,8 +98,8 @@ def _scan_methods_in_class(self, class_name: str, _class: Any) -> None:
self.signatures[full_path] = inspect.signature(method)


def _print(signature: Dict[str, inspect.Signature], variables: Set[str]) -> None:
result: Dict[str, Any] = {"routines": {}, "variables": sorted(variables)}
def _print(signature: dict[str, inspect.Signature], variables: set[str]) -> None:
result: dict[str, Any] = {"routines": {}, "variables": sorted(variables)}
for key, value in signature.items():
result["routines"][key] = [
(
Expand All @@ -116,23 +117,23 @@ def _print(signature: Dict[str, inspect.Signature], variables: Set[str]) -> None


class _BreakingChanges(NamedTuple):
deleted_variables: List[str]
deleted_routines: List[str]
incompatible_routines: List[str]
deleted_variables: list[str]
deleted_routines: list[str]
incompatible_routines: list[str]

def is_empty(self) -> bool:
return not any([self.deleted_variables, self.deleted_routines, self.incompatible_routines])

@staticmethod
def _argument_to_str(argument: Dict[str, Any]) -> str:
def _argument_to_str(argument: dict[str, Any]) -> str:
if "default" in argument:
return f'{argument["name"]}={argument["default"]}'
return str(argument["name"])

def print_markdown(
self,
original_routines: Dict[str, List[Dict[str, Any]]],
routines: Dict[str, List[Dict[str, Any]]],
original_routines: dict[str, list[dict[str, Any]]],
routines: dict[str, list[dict[str, Any]]],
) -> None:
"""Print all breaking changes in markdown."""
print("\n# Compatibility breaking changes:")
Expand All @@ -156,7 +157,7 @@ def print_markdown(


def _only_new_optional_arguments_or_existing_arguments_optionalized_or_var_arguments(
original_arguments: List[Dict[str, Any]], arguments: List[Dict[str, Any]]
original_arguments: list[dict[str, Any]], arguments: list[dict[str, Any]]
) -> bool:
if len(original_arguments) > len(arguments):
return False
Expand All @@ -178,7 +179,7 @@ def _only_new_optional_arguments_or_existing_arguments_optionalized_or_var_argum
)


def _is_compatible(original_arguments: List[Dict[str, Any]], arguments: List[Dict[str, Any]]) -> bool:
def _is_compatible(original_arguments: list[dict[str, Any]], arguments: list[dict[str, Any]]) -> bool:
"""
If there is an argument change, it is compatible only when
- new optional arguments are added or existing arguments become optional.
Expand All @@ -201,13 +202,13 @@ def _is_compatible(original_arguments: List[Dict[str, Any]], arguments: List[Dic


def _detect_breaking_changes(
original_routines: Dict[str, List[Dict[str, Any]]],
original_variables: Set[str],
routines: Dict[str, List[Dict[str, Any]]],
variables: Set[str],
original_routines: dict[str, list[dict[str, Any]]],
original_variables: set[str],
routines: dict[str, list[dict[str, Any]]],
variables: set[str],
) -> _BreakingChanges:
deleted_routines: List[str] = []
incompatible_routines: List[str] = []
deleted_routines: list[str] = []
incompatible_routines: list[str] = []
for routine_path, arguments in original_routines.items():
if routine_path not in routines:
deleted_routines.append(routine_path)
Expand Down
4 changes: 2 additions & 2 deletions bin/sam-translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

Known limitations: cannot transform CodeUri pointing at local directory.
"""

import argparse
import json
import logging
Expand All @@ -12,7 +13,6 @@
import sys
from functools import reduce
from pathlib import Path
from typing import List

import boto3

Expand Down Expand Up @@ -71,7 +71,7 @@
logging.basicConfig()


def execute_command(command: str, args: List[str]) -> None:
def execute_command(command: str, args: list[str]) -> None:
try:
aws_cmd = "aws" if platform.system().lower() != "windows" else "aws.cmd"
command_with_args = [aws_cmd, "cloudformation", command, *list(args)]
Expand Down
5 changes: 3 additions & 2 deletions bin/transform-test-error-json-format.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
It makes error json easier to review by breaking down "errorMessage"
into list of strings (delimiter: ". ").
"""

import sys
from pathlib import Path

# To allow this script to be executed from other directories
sys.path.insert(0, str(Path(__file__).absolute().parent.parent))

import json
from typing import Final, Type
from typing import Final

from bin._file_formatter import FileFormatter

Expand Down Expand Up @@ -41,7 +42,7 @@ def format_str(self, input_str: str) -> str:
return json.dumps(obj, indent=2, sort_keys=True) + "\n"

@staticmethod
def decode_exception() -> Type[Exception]:
def decode_exception() -> type[Exception]:
return json.JSONDecodeError

@staticmethod
Expand Down
Loading
Loading