Skip to content

Commit

Permalink
Deprecate Python 3.7
Browse files Browse the repository at this point in the history
  • Loading branch information
bhirsz committed May 6, 2024
1 parent 83d9087 commit 01aba0f
Show file tree
Hide file tree
Showing 31 changed files with 190 additions and 159 deletions.
5 changes: 1 addition & 4 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,9 @@ jobs:
- os: 'windows-latest'
python-version: '3.9'
rf-version: 'rf5'
- os: 'ubuntu-latest'
python-version: '3.7'
rf-version: 'rf4'
- os: 'ubuntu-latest'
python-version: '3.8'
rf-version: 'rf5'
rf-version: 'rf4'
- os: 'ubuntu-latest'
python-version: '3.9'
rf-version: 'rf5'
Expand Down
5 changes: 5 additions & 0 deletions docs/releasenotes/unreleased/other.5.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Deprecated support for 3.7 (#570)
---------------------------------

We dropped support for Python 3.7 as it is reached end of lifecycle. It allowed us to refactor our code to use more
recent Python features like improved typing.
7 changes: 4 additions & 3 deletions robotidy/api.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"""
Methods for transforming Robot Framework ast model programmatically.
"""
from __future__ import annotations

from pathlib import Path
from typing import Optional

from robotidy import app, disablers, files
from robotidy.config import MainConfig, RawConfig


def get_robotidy(src: str, output: Optional[str], ignore_git_dir: bool = False, **kwargs):
def get_robotidy(src: str, output: str | None, ignore_git_dir: bool = False, **kwargs):
config = RawConfig(**kwargs)
config_file = files.find_source_config_file(Path(src), ignore_git_dir)
if config_file:
Expand All @@ -24,7 +25,7 @@ def get_robotidy(src: str, output: Optional[str], ignore_git_dir: bool = False,
return app.Robotidy(main_config)


def transform_model(model, root_dir: str, output: Optional[str] = None, **kwargs) -> Optional[str]:
def transform_model(model, root_dir: str, output: str | None = None, **kwargs) -> str | None:
"""
:param model: The model to be transformed.
:param root_dir: Root directory. Configuration file is searched based
Expand Down
2 changes: 1 addition & 1 deletion robotidy/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def transform_files(self):
elif self.config.verbose:
click.echo(f"Found {source} file")
model = self.get_model(source)
model_path = model.source
model_path = model.source or source
disabler_finder.visit(model)
if disabler_finder.is_disabled_in_file(disablers.ALL_TRANSFORMERS):
continue
Expand Down
18 changes: 9 additions & 9 deletions robotidy/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

import sys
from pathlib import Path
from typing import List, Optional, Pattern, Tuple, Union
from typing import Pattern

try:
import rich_click as click
Expand Down Expand Up @@ -89,17 +91,17 @@
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])


def validate_regex_callback(ctx: click.Context, param: click.Parameter, value: Optional[str]) -> Optional[Pattern]:
def validate_regex_callback(ctx: click.Context, param: click.Parameter, value: str | None) -> Pattern | None:
return misc.validate_regex(value)


def validate_target_version_callback(
ctx: click.Context, param: Union[click.Option, click.Parameter], value: Optional[str]
) -> Optional[int]:
ctx: click.Context, param: click.Option | click.Parameter, value: str | None
) -> int | None:
return validate_target_version(value)


def validate_list_optional_value(ctx: click.Context, param: Union[click.Option, click.Parameter], value: Optional[str]):
def validate_list_optional_value(ctx: click.Context, param: click.Option | click.Parameter, value: str | None):
if not value:
return value
allowed = ["all", "enabled", "disabled"]
Expand All @@ -108,9 +110,7 @@ def validate_list_optional_value(ctx: click.Context, param: Union[click.Option,
return value


def csv_list_type_callback(
ctx: click.Context, param: Union[click.Option, click.Parameter], value: Optional[str]
) -> List[str]:
def csv_list_type_callback(ctx: click.Context, param: click.Option | click.Parameter, value: str | None) -> list[str]:
return csv_list_type(value)


Expand Down Expand Up @@ -139,7 +139,7 @@ def print_description(name: str, target_version: int):
return 0


def _load_external_transformers(transformers: List, transformers_config: TransformConfigMap, target_version: int):
def _load_external_transformers(transformers: list, transformers_config: TransformConfigMap, target_version: int):
external = []
transformers_names = {transformer.name for transformer in transformers}
transformers_from_conf = load_transformers(transformers_config, target_version=target_version)
Expand Down
56 changes: 29 additions & 27 deletions robotidy/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import copy
import dataclasses
import os
Expand All @@ -6,7 +8,7 @@
from collections import namedtuple
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Pattern, Set, Tuple
from typing import Pattern

try:
from robot.api import Languages # RF 6.0
Expand All @@ -25,11 +27,11 @@ class FormattingConfig:
def __init__(
self,
space_count: int,
indent: Optional[int],
continuation_indent: Optional[int],
indent: int | None,
continuation_indent: int | None,
line_sep: str,
start_line: Optional[int],
end_line: Optional[int],
start_line: int | None,
end_line: int | None,
separator: str,
line_length: int,
):
Expand Down Expand Up @@ -66,7 +68,7 @@ def get_line_sep(line_sep):
return os.linesep


def validate_target_version(value: Optional[str]) -> Optional[int]:
def validate_target_version(value: str | None) -> int | None:
if value is None:
return misc.ROBOT_VERSION.major
try:
Expand All @@ -82,19 +84,19 @@ def validate_target_version(value: Optional[str]) -> Optional[int]:
return target_version


def csv_list_type(value: Optional[str]) -> List[str]:
def csv_list_type(value: str | None) -> list[str]:
if not value:
return []
return value.split(",")


def convert_transformers_config(
param_name: str,
config: Dict,
config: dict,
force_included: bool = False,
custom_transformer: bool = False,
is_config: bool = False,
) -> List[TransformConfig]:
) -> list[TransformConfig]:
return [
TransformConfig(tr, force_include=force_included, custom_transformer=custom_transformer, is_config=is_config)
for tr in config.get(param_name, ())
Expand All @@ -120,10 +122,10 @@ def map_class_fields_with_their_types(cls):
class RawConfig:
"""Configuration read directly from cli or configuration file."""

transform: List[TransformConfig] = field(default_factory=list)
custom_transformers: List[TransformConfig] = field(default_factory=list)
configure: List[TransformConfig] = field(default_factory=list)
src: Tuple[str, ...] = None
transform: list[TransformConfig] = field(default_factory=list)
custom_transformers: list[TransformConfig] = field(default_factory=list)
configure: list[TransformConfig] = field(default_factory=list)
src: tuple[str, ...] = None
exclude: Pattern = re.compile(files.DEFAULT_EXCLUDES)
extend_exclude: Pattern = None
skip_gitignore: bool = False
Expand All @@ -148,14 +150,14 @@ class RawConfig:
output: Path = None
force_order: bool = False
target_version: int = misc.ROBOT_VERSION.major
language: List[str] = field(default_factory=list)
language: list[str] = field(default_factory=list)
reruns: int = 0
ignore_git_dir: bool = False
skip_comments: bool = False
skip_documentation: bool = False
skip_return_values: bool = False
skip_keyword_call: List[str] = None
skip_keyword_call_pattern: List[str] = None
skip_keyword_call: list[str] = None
skip_keyword_call_pattern: list[str] = None
skip_settings: bool = False
skip_arguments: bool = False
skip_setup: bool = False
Expand All @@ -166,8 +168,8 @@ class RawConfig:
skip_tags: bool = False
skip_block_comments: bool = False
skip_sections: str = ""
defined_in_cli: Set = field(default_factory=set)
defined_in_config: Set = field(default_factory=set)
defined_in_cli: set = field(default_factory=set)
defined_in_config: set = field(default_factory=set)

@classmethod
def from_cli(cls, ctx: click.Context, **kwargs):
Expand All @@ -178,7 +180,7 @@ def from_cli(cls, ctx: click.Context, **kwargs):
defined_in_cli.add(option)
return cls(**kwargs, defined_in_cli=defined_in_cli)

def from_config_file(self, config: Dict, config_path: Path) -> "RawConfig":
def from_config_file(self, config: dict, config_path: Path) -> "RawConfig":
"""Creates new RawConfig instance from dictionary.
Dictionary key:values needs to be normalized and parsed to correct types.
Expand All @@ -192,19 +194,19 @@ def from_config_file(self, config: Dict, config_path: Path) -> "RawConfig":
if key not in options_map:
raise exceptions.NoSuchOptionError(key, list(options_map.keys())) from None
value_type = options_map[key]
if value_type == bool:
if value_type == "bool": # will not be required for basic types after Python 3.9
parsed_config[key] = str_to_bool(value)
elif key == "target_version":
parsed_config[key] = validate_target_version(value)
elif key == "language":
parsed_config[key] = csv_list_type(value)
elif value_type == int:
elif value_type == "int":
parsed_config[key] = int(value)
elif value_type == List[TransformConfig]:
elif value_type == "list[TransformConfig]":
parsed_config[key] = [convert_transform_config(val, key) for val in value]
elif key == "src":
parsed_config[key] = tuple(value)
elif value_type == Pattern:
elif value_type in ("Pattern", Pattern): # future typing for 3.8 provides type as str
parsed_config[key] = misc.validate_regex(value)
else:
parsed_config[key] = value
Expand Down Expand Up @@ -250,7 +252,7 @@ def load_config_from_option(cli_config: RawConfig) -> RawConfig:
cli_config = cli_config.from_config_file(config_file, config_path)
return cli_config

def get_sources(self, sources: Tuple[str, ...]) -> Optional[Tuple[str, ...]]:
def get_sources(self, sources: tuple[str, ...]) -> tuple[str, ...] | None:
"""Get list of sources to be transformed by Robotidy.
If the sources tuple is empty, look for most common configuration file and load sources from there.
Expand Down Expand Up @@ -306,13 +308,13 @@ def __init__(
show_diff: bool,
verbose: bool,
check: bool,
output: Optional[Path],
output: Path | None,
force_order: bool,
target_version: int,
color: bool,
language: Optional[List[str]],
language: list[str] | None,
reruns: int,
config_path: Optional[Path],
config_path: Path | None,
):
self.formatting = formatting
self.overwrite = self.set_overwrite_mode(overwrite, check)
Expand Down
5 changes: 3 additions & 2 deletions robotidy/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import sys
from typing import List

from click import NoSuchOption

Expand Down Expand Up @@ -51,7 +52,7 @@ def __init__(self):


class NoSuchOptionError(NoSuchOption):
def __init__(self, option_name: str, allowed_options: List[str]):
def __init__(self, option_name: str, allowed_options: list[str]):
rec_finder = misc.RecommendationFinder()
similar = rec_finder.find(option_name, allowed_options)
super().__init__(option_name, possibilities=similar)
30 changes: 15 additions & 15 deletions robotidy/files.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

from functools import lru_cache
from pathlib import Path
from typing import Any, Dict, Iterable, Iterator, List, Optional, Pattern, Tuple
from typing import Any, Iterable, Iterator, Pattern

import pathspec

Expand All @@ -18,7 +20,7 @@


@lru_cache()
def find_source_config_file(src: Path, ignore_git_dir: bool = False) -> Optional[Path]:
def find_source_config_file(src: Path, ignore_git_dir: bool = False) -> Path | None:
"""Find and return configuration file for the source path.
This method looks iteratively in source parents for directory that contains configuration file and
Expand All @@ -40,7 +42,7 @@ def find_source_config_file(src: Path, ignore_git_dir: bool = False) -> Optional


@lru_cache()
def find_project_root(srcs: Tuple[str], ignore_git_dir: bool = False) -> Path:
def find_project_root(srcs: tuple[str, ...], ignore_git_dir: bool = False) -> Path:
"""Return a directory containing .git, or robotidy.toml.
That directory will be a common parent of all files and directories
passed in `srcs`.
Expand Down Expand Up @@ -69,7 +71,7 @@ def find_project_root(srcs: Tuple[str], ignore_git_dir: bool = False) -> Path:
return directory


def load_toml_file(config_path: Path) -> Dict[str, Any]:
def load_toml_file(config_path: Path) -> dict[str, Any]:
try:
with config_path.open("rb") as tf:
config = tomli.load(tf)
Expand All @@ -78,7 +80,7 @@ def load_toml_file(config_path: Path) -> Dict[str, Any]:
raise click.FileError(filename=str(config_path), hint=f"Error reading configuration file: {e}")


def read_pyproject_config(config_path: Path) -> Dict[str, Any]:
def read_pyproject_config(config_path: Path) -> dict[str, Any]:
config = load_toml_file(config_path)
if config_path.name != DOTFILE_CONFIG or "tool" in config:
config = config.get("tool", {}).get("robotidy", {})
Expand All @@ -89,7 +91,7 @@ def read_pyproject_config(config_path: Path) -> Dict[str, Any]:
def get_gitignore(root: Path) -> pathspec.PathSpec:
"""Return a PathSpec matching gitignore content if present."""
gitignore = root / ".gitignore"
lines: List[str] = []
lines: list[str] = []
if gitignore.is_file():
with gitignore.open(encoding="utf-8") as gf:
lines = gf.readlines()
Expand All @@ -99,9 +101,9 @@ def get_gitignore(root: Path) -> pathspec.PathSpec:
def should_parse_path(
path: Path,
root_parent: Path,
exclude: Optional[Pattern[str]],
extend_exclude: Optional[Pattern[str]],
gitignore: Optional[pathspec.PathSpec],
exclude: Pattern[str] | None,
extend_exclude: Pattern[str] | None,
gitignore: pathspec.PathSpec | None,
) -> bool:
normalized_path = str(path)
for pattern in (exclude, extend_exclude):
Expand All @@ -126,9 +128,7 @@ def get_path_relative_to_project_root(path: Path, root_parent: Path) -> Path:
return path


def get_paths(
src: Tuple[str, ...], exclude: Optional[Pattern], extend_exclude: Optional[Pattern], skip_gitignore: bool
):
def get_paths(src: tuple[str, ...], exclude: Pattern | None, extend_exclude: Pattern | None, skip_gitignore: bool):
root = find_project_root(src)
if skip_gitignore:
gitignore = None
Expand All @@ -155,10 +155,10 @@ def get_paths(

def iterate_dir(
paths: Iterable[Path],
exclude: Optional[Pattern],
extend_exclude: Optional[Pattern],
exclude: Pattern | None,
extend_exclude: Pattern | None,
root_parent: Path,
gitignore: Optional[pathspec.PathSpec],
gitignore: pathspec.PathSpec | None,
) -> Iterator[Path]:
for path in paths:
if not should_parse_path(path, root_parent, exclude, extend_exclude, gitignore):
Expand Down
Loading

0 comments on commit 01aba0f

Please sign in to comment.