Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make pyre able to read configuration from pyproject.toml #799

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/pyre.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
pip install --upgrade pip
pip install -r requirements.txt
pip install cython flask flask_cors graphql-core typing_inspect
VERSION=$(grep "version" .pyre_configuration | sed -n -e 's/.*\(0\.0\.[0-9]*\).*/\1/p')
VERSION=$(grep "version" pyproject.toml | sed -n -e 's/.*\(0\.0\.[0-9]*\).*/\1/p')
pip install pyre-check-nightly==$VERSION

- name: Run Pyre
Expand Down
3 changes: 3 additions & 0 deletions .pyre_configuration
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
{
"is_toplevel_module": true,
"site-package": "typing_inspect"
},
{
"site-package": "tomli"
Copy link
Contributor

@cclauss cclauss Apr 24, 2024

Choose a reason for hiding this comment

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

tomli should be used only on Python < 3.11 because in Python >= 3.11 tomllib is in the Standard Library.

}
],
"source_directories": [
Expand Down
39 changes: 34 additions & 5 deletions client/commands/initialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@
from pathlib import Path
from typing import Any, Dict, Optional, Tuple, Union

import tomli

from .. import log

from ..find_directories import (
BINARY_NAME,
CONFIGURATION_FILE,
find_global_root,
find_parent_directory_containing_file,
find_taint_models_directory,
find_typeshed,
JSON_CONFIGURATION_FILE,
LOCAL_CONFIGURATION_FILE,
TOML_CONFIGURATION_FILE,
)
from . import commands

Expand All @@ -55,7 +58,7 @@ def _create_source_directory_element(source: str) -> Union[str, Dict[str, str]]:
return source


def _check_configuration_file_location(
def _check_json_configuration_file_location(
configuration_path: Path, current_directory: Path, global_root: Optional[Path]
) -> None:
if os.path.isfile(configuration_path):
Expand All @@ -79,6 +82,20 @@ def _check_configuration_file_location(
)


def _check_toml_configuration_file_content(
configuration_path: Path, global_root: Optional[Path]
) -> None:
if configuration_path.is_file() and not global_root:
toml_configurations = tomli.loads(configuration_path.read_text())
try:
toml_configurations["tool"]["pyre"]
except KeyError:
return
raise InitializationException(
f"A Pyre configuration already exists at {str(configuration_path)}"
)


def _get_local_configuration(
current_directory: Path, buck_root: Optional[Path]
) -> Dict[str, Any]:
Expand Down Expand Up @@ -216,15 +233,27 @@ def get_configuration_and_path(
Path("."), ".buckconfig"
)
current_directory: Path = Path(os.getcwd())
configuration_path = current_directory / CONFIGURATION_FILE
_check_configuration_file_location(
configuration_path, current_directory, global_root
configuration_path_json = current_directory / JSON_CONFIGURATION_FILE
configuration_path_toml = current_directory / TOML_CONFIGURATION_FILE
_check_json_configuration_file_location(
configuration_path_json, current_directory, global_root
)
_check_toml_configuration_file_content(configuration_path_toml, global_root)
local_configuration_path = current_directory / LOCAL_CONFIGURATION_FILE
if global_root:
configuration_path = local_configuration_path
configuration = _get_local_configuration(current_directory, buck_root)
else:
where_to_store = log.get_input(
f"Should pyre store configuration at `{JSON_CONFIGURATION_FILE}` or `{TOML_CONFIGURATION_FILE}`?",
suffix="Type `JSON` or `TOML` to answer(lower case work as well).Default is `JSON`",
).upper()
if where_to_store in ("JSON", ""):
configuration_path = current_directory / JSON_CONFIGURATION_FILE
elif where_to_store == "TOML":
configuration_path = configuration_path_toml / TOML_CONFIGURATION_FILE
else:
raise InitializationException("Please answer `JSON` or `TOML`.")
configuration = _get_configuration(taint_models_directory_required)
return configuration, configuration_path

Expand Down
2 changes: 1 addition & 1 deletion client/commands/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def get_full_path(root: str, relative: str) -> str:
configuration_name = (
find_directories.CODENAV_CONFIGURATION_FILE
if flavor == identifiers.PyreFlavor.CODE_NAVIGATION
else find_directories.CONFIGURATION_FILE
else find_directories.JSON_CONFIGURATION_FILE
)
local_root = configuration.get_local_root()
return [
Expand Down
44 changes: 33 additions & 11 deletions client/configuration/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@
)

import psutil
import tomli
Fixed Show fixed Hide fixed

from .. import command_arguments, dataclasses_merge, find_directories, identifiers
from ..filesystem import expand_global_root, expand_relative_path
from ..find_directories import (
CONFIGURATION_FILE,
get_relative_local_root,
JSON_CONFIGURATION_FILE,
LOCAL_CONFIGURATION_FILE,
TOML_CONFIGURATION_FILE,
)
from . import (
exceptions,
Expand Down Expand Up @@ -245,7 +247,9 @@ def from_command_arguments(
)

@staticmethod
def from_string(contents: str) -> "PartialConfiguration":
def from_string(
contents: str, is_pyproject_dot_toml: bool = False
) -> "PartialConfiguration":
def is_list_of_string(elements: object) -> bool:
return isinstance(elements, list) and all(
isinstance(element, str) for element in elements
Expand Down Expand Up @@ -332,7 +336,13 @@ def create_search_paths(
return search_path

try:
configuration_json = json.loads(contents)
if is_pyproject_dot_toml:
# For usual, we write pyproject.toml as
# [tool.pyre]
# ...<configurations>
configuration_json = tomli.loads(contents)["tool"]["pyre"]
else:
configuration_json = json.loads(contents)

dot_pyre_directory = ensure_option_type(
configuration_json, "dot_pyre_directory", str
Expand Down Expand Up @@ -489,14 +499,18 @@ def create_search_paths(
LOG.warning(f"Unrecognized configuration item: {unrecognized_key}")

return partial_configuration
except json.JSONDecodeError as error:
raise exceptions.InvalidConfiguration("Invalid JSON file") from error
except (json.JSONDecodeError, tomli.TOMLDecodeError) as error:
raise exceptions.InvalidConfiguration(
f'Invalid {"TOML" if is_pyproject_dot_toml else "JSON"} file'
) from error

@staticmethod
def from_file(path: Path) -> "PartialConfiguration":
def from_file(
path: Path, is_pyproject_dot_toml: bool = False
) -> "PartialConfiguration":
try:
contents = path.read_text(encoding="utf-8")
return PartialConfiguration.from_string(contents)
return PartialConfiguration.from_string(contents, is_pyproject_dot_toml)
except OSError as error:
raise exceptions.InvalidConfiguration(
f"Error when reading {path}"
Expand Down Expand Up @@ -911,7 +925,7 @@ def create_configuration(
if found_root is None:
raise exceptions.InvalidConfiguration(
"A local configuration path was explicitly specified, but no"
+ f" {CONFIGURATION_FILE} file was found in {search_base}"
+ f" {JSON_CONFIGURATION_FILE} file was found in {search_base}"
+ " or its parents."
)
elif found_root.local_root is None:
Expand All @@ -931,9 +945,17 @@ def create_configuration(
else:
project_root = found_root.global_root
relative_local_root = None
partial_configuration = PartialConfiguration.from_file(
project_root / CONFIGURATION_FILE
).expand_relative_paths(str(project_root))
if (project_root / JSON_CONFIGURATION_FILE).is_file():
partial_configuration = PartialConfiguration.from_file(
project_root / JSON_CONFIGURATION_FILE
).expand_relative_paths(str(project_root))
else:
LOG.debug(
"Could not find `.pyre_configuration` in the project root.Searching for `pyproject.toml`..."
)
partial_configuration = PartialConfiguration.from_file(
project_root / TOML_CONFIGURATION_FILE, is_pyproject_dot_toml=True
).expand_relative_paths(str(project_root))
local_root = found_root.local_root
if local_root is not None:
relative_local_root = get_relative_local_root(project_root, local_root)
Expand Down
20 changes: 19 additions & 1 deletion client/configuration/tests/configuration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import testslide

from ... import command_arguments
from ...find_directories import CODENAV_CONFIGURATION_FILE
from ...find_directories import CODENAV_CONFIGURATION_FILE, TOML_CONFIGURATION_FILE
from ...tests.setup import (
ensure_directories_exists,
ensure_files_exist,
Expand Down Expand Up @@ -1210,3 +1210,21 @@ def test_source_directories_glob(self) -> None:
SimpleElement(str(root_path / "b")),
],
)

def test_pyproject_dot_toml_configuration(self) -> None:
with tempfile.TemporaryDirectory() as root:
root_path = Path(root)
pyproject_config: str = """
[tool.pyre]
exclude = ["This configuration only for test reading data from pyproject.toml"]
"""
with open(
root_path / TOML_CONFIGURATION_FILE, "w", encoding="UTF-8"
) as configuration_file:
configuration_file.write(pyproject_config)
self.assertEqual(
PartialConfiguration.from_file(
root_path / TOML_CONFIGURATION_FILE, is_pyproject_dot_toml=True
).excludes,
["This configuration only for test reading data from pyproject.toml"],
)
9 changes: 6 additions & 3 deletions client/find_directories.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
from typing import Callable, List, NamedTuple, Optional


CONFIGURATION_FILE: str = ".pyre_configuration"
JSON_CONFIGURATION_FILE: str = ".pyre_configuration"
TOML_CONFIGURATION_FILE: str = "pyproject.toml"
LOCAL_CONFIGURATION_FILE: str = ".pyre_configuration.local"
CODENAV_CONFIGURATION_FILE: str = ".pyre_configuration.codenav"
BINARY_NAME: str = "pyre.bin"
Expand Down Expand Up @@ -123,7 +124,7 @@ def find_outermost_directory_containing_file(
def find_global_root(base: Path) -> Optional[Path]:
"""Pyre always runs from the directory containing the nearest .pyre_configuration,
if one exists."""
return find_parent_directory_containing_file(base, CONFIGURATION_FILE)
return find_parent_directory_containing_file(base, JSON_CONFIGURATION_FILE)


def get_relative_local_root(
Expand Down Expand Up @@ -153,7 +154,9 @@ def find_global_and_local_root(base: Path) -> Optional[FoundRoot]:
return the path to the global configuration.
If both global and local exist, return them as a pair.
"""
found_global_root = find_parent_directory_containing_file(base, CONFIGURATION_FILE)
found_global_root = find_parent_directory_containing_file(
base, JSON_CONFIGURATION_FILE
)
if found_global_root is None:
return None

Expand Down
4 changes: 2 additions & 2 deletions client/tests/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

from ..find_directories import (
CODENAV_CONFIGURATION_FILE,
CONFIGURATION_FILE,
JSON_CONFIGURATION_FILE,
LOCAL_CONFIGURATION_FILE,
)

Expand Down Expand Up @@ -67,7 +67,7 @@ def write_configuration_file(
if codenav:
configuration_file = CODENAV_CONFIGURATION_FILE
else:
configuration_file = CONFIGURATION_FILE
configuration_file = JSON_CONFIGURATION_FILE
if relative is None:
(root / configuration_file).write_text(json.dumps(content))
else:
Expand Down
34 changes: 34 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ commands =
usort check client scripts tools
flake8 client scripts tools
"""

[tool.ufmt]
excludes = [
"documentation/",
Expand All @@ -36,3 +37,36 @@ excludes = [
"source/",
"stubs/"
]

[tool.pyre]
exclude = [
".*/documentation/.*",
".*/generate_taint_models/.*",
".*/scripts/.*",
".*/source/.*",
".*/pyre-check/stubs/.*"
]
ignore_all_errors = [
"client/tests/configuration_test.py",
"pyre_extensions/safe_json.py"
]
search_path = [
{ "site-package" = "click" },
{ "site-package" = "dataclasses_json"},
{ "site-package" = "flask" },
{ "site-package" = "flask_cors" },
{ "site-package" = "graphql" },
{ "site-package" = "intervaltree" },
{ "site-package" = "libcst" },
{ "site-package" = "marshmallow" },
{ "site-package" = "psutil" },
{ "site-package" = "testslide" },
{ "is_toplevel_module" = true, "site-package" = "typing_extensions" },
{ "is_toplevel_module" = true, "site-package" = "typing_inspect" },
{ "site-package" = "tomli" }
]
source_directories = [
"."
]
strict = true
version = "0.0.101695726714"
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ tabulate
testslide>=2.7.0
typing_extensions
typing_inspect
tomli
4 changes: 2 additions & 2 deletions tools/upgrade/commands/strict_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
from pyre_extensions import override

from ....client.find_directories import (
CONFIGURATION_FILE,
find_global_and_local_root,
JSON_CONFIGURATION_FILE,
LOCAL_CONFIGURATION_FILE,
)
from .. import UserError
Expand All @@ -40,7 +40,7 @@ def _get_configuration_path(local_configuration: Optional[Path]) -> Optional[Pat
if local_root:
return local_root / LOCAL_CONFIGURATION_FILE
else:
return found_root.global_root / CONFIGURATION_FILE
return found_root.global_root / JSON_CONFIGURATION_FILE


class StrictDefault(ErrorSuppressingCommand):
Expand Down
Loading