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

Replacing "print" with "logging" Module #974

Merged
merged 51 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
08c3d3c
init logging
merrickliu888 Oct 10, 2023
fed6c77
formatting
merrickliu888 Oct 10, 2023
385cde6
contracts logger
merrickliu888 Oct 10, 2023
f3761bf
config logger
merrickliu888 Oct 10, 2023
8f33d56
contracts logging test
merrickliu888 Oct 10, 2023
b83198b
cleaning up comments
merrickliu888 Oct 10, 2023
91b64d5
adding return val
merrickliu888 Oct 10, 2023
c0d1375
removing original prefixes for msgs
merrickliu888 Oct 10, 2023
007c3fa
Adding logging
merrickliu888 Oct 10, 2023
3c1dc0e
Adding logging config back and taking out last print statement
merrickliu888 Oct 11, 2023
0ee4c29
Top level init tests
merrickliu888 Oct 12, 2023
13e63f9
cleaning up TODOs
merrickliu888 Oct 20, 2023
81e166a
init version test
merrickliu888 Oct 20, 2023
9aff6a9
tests
merrickliu888 Oct 20, 2023
86ecdca
Merge
merrickliu888 Oct 20, 2023
bc13e8c
test_load_messages test fixed using patch
merrickliu888 Oct 29, 2023
ab7a72c
test_load_messages test fixed using patch
merrickliu888 Nov 3, 2023
f18f687
Reformatting
merrickliu888 Nov 28, 2023
12edcc6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 28, 2023
e6b09e3
test config done
merrickliu888 Nov 28, 2023
8c00984
test valid files done
merrickliu888 Nov 28, 2023
e469668
test _check logging finished
merrickliu888 Nov 28, 2023
972ec39
adding pylint_comment example file and finish testing
merrickliu888 Nov 28, 2023
c95b494
merge
merrickliu888 Nov 28, 2023
36c4c38
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 28, 2023
ed5ff7d
merge
merrickliu888 Nov 30, 2023
a38c471
pathing fix
merrickliu888 Dec 2, 2023
4949be1
init logging fix
merrickliu888 Dec 2, 2023
8f5cce3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 2, 2023
76f1b4e
init logging fix
merrickliu888 Dec 5, 2023
7cd3045
init logging unicode bug fix
merrickliu888 Dec 5, 2023
e4128be
Merge branch 'logging' of https://github.com/merrickliu888/pyta-fork …
merrickliu888 Dec 5, 2023
a59aaf5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 5, 2023
1fb8b82
fixing paths
merrickliu888 Dec 6, 2023
9a0bc41
fixing z3-solver version issue
merrickliu888 Dec 6, 2023
c527359
adding back path
merrickliu888 Dec 6, 2023
8701320
Merge branch 'logging' of https://github.com/merrickliu888/pyta-fork …
merrickliu888 Dec 6, 2023
3e7ce89
fixing tests after commit hooks
merrickliu888 Dec 6, 2023
b13d09c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 6, 2023
e5dd549
adding monkepatch.undo()
merrickliu888 Dec 7, 2023
4523746
Merge branch 'logging' of https://github.com/merrickliu888/pyta-fork …
merrickliu888 Dec 7, 2023
91125e0
merge
merrickliu888 Dec 7, 2023
2a3e58c
fixing space
merrickliu888 Dec 7, 2023
01bfeb5
moving version logging
merrickliu888 Dec 7, 2023
efd7245
fixing comment
merrickliu888 Dec 7, 2023
6e7ea88
fixing init testing
merrickliu888 Dec 7, 2023
88c4cc8
spacing
merrickliu888 Dec 7, 2023
4a580c4
removing import
merrickliu888 Dec 7, 2023
e9c25f1
Merge remote-tracking branch 'origin/master' into logging
david-yz-liu Dec 10, 2023
d8ff8a9
Move logging config into _check function
david-yz-liu Dec 10, 2023
6720186
Some updates to logging branch.
david-yz-liu Dec 11, 2023
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
56 changes: 30 additions & 26 deletions python_ta/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
pass

import importlib.util
import logging
import os
import sys
import tokenize
Expand All @@ -53,11 +54,14 @@
from .reporters import REPORTERS
from .upload import upload_to_server

# Configuring logger
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at this, I realized that we really shouldn't have this top-level code at all.

  • Move this to the top of the _check method.
  • Move the sys.version_info < (3, 7, 0) part there as well.
  • I realize this actually makes your sys.version_info test a lot simpler...! 😅

logging.basicConfig(format="[%(levelname)s] %(message)s", level=logging.NOTSET)

HELP_URL = "http://www.cs.toronto.edu/~david/pyta/checkers/index.html"

# check the python version
if sys.version_info < (3, 7, 0):
print("[WARNING] You need Python 3.7 or later to run PythonTA.")
logging.warning("You need Python 3.7 or later to run PythonTA.")


# Flag to determine if we've previously patched pylint
Expand Down Expand Up @@ -174,17 +178,15 @@ def _check(
current_reporter.print_messages(level)
if linter.config.pyta_file_permission:
f_paths.append(file_py) # Appending paths for upload
print(
"[INFO] File: {} was checked using the configuration file: {}".format(
logging.info(
"File: {} was checked using the configuration file: {}".format(
file_py, linter.config_file
),
file=sys.stderr,
)
)
print(
"[INFO] File: {} was checked using the messages-config file: {}".format(
logging.info(
"File: {} was checked using the messages-config file: {}".format(
file_py, messages_config_path
),
file=sys.stderr,
)
)
if linter.config.pyta_error_permission:
errs = list(current_reporter.messages.values())
Expand All @@ -206,10 +208,10 @@ def _check(
linter.generate_reports()
return current_reporter
except Exception as e:
print(
"[ERROR] Unexpected error encountered! Please report this to your instructor (and attach the code that caused the error)."
logging.error(
"Unexpected error encountered! Please report this to your instructor (and attach the code that caused the error)."
)
print('[ERROR] Error message: "{}"'.format(e))
logging.error('Error message: "{}"'.format(e))
raise e


Expand Down Expand Up @@ -386,32 +388,32 @@ def _verify_pre_check(filepath: AnyStr, allow_pylint_comments: bool) -> bool:
continue
match = OPTION_PO.search(content)
if match is not None:
print(
'[ERROR] String "pylint:" found in comment. '
logging.error(
'String "pylint:" found in comment. '
+ "No check run on file `{}.`\n".format(filepath)
)
return False
except IndentationError as e:
print(
"[ERROR] python_ta could not check your code due to an "
logging.error(
"python_ta could not check your code due to an "
+ "indentation error at line {}.".format(e.lineno)
)
return False
except tokenize.TokenError as e:
print(
"[ERROR] python_ta could not check your code due to a " + "syntax error in your file."
logging.error(
"python_ta could not check your code due to a " + "syntax error in your file."
)
return False
except UnicodeDecodeError:
print(
"[ERROR] python_ta could not check your code due to an "
logging.error(
"python_ta could not check your code due to an "
+ "invalid character. Please check the following lines "
"in your file and all characters that are marked with a �."
)
with open(os.path.expanduser(filepath), encoding="utf-8", errors="replace") as f:
for i, line in enumerate(f):
if "�" in line:
print(f" Line {i}: {line}", end="")
logging.error(f" Line {i + 1}: {line}")
return False
return True

Expand All @@ -428,7 +430,7 @@ def _get_valid_files_to_check(module_name: Union[List[str], str]) -> Generator[A
module_name = [module_name]
# Otherwise, enforce API to expect `module_name` type as list
elif not isinstance(module_name, list):
print(
logging.error(
"No checks run. Input to check, `{}`, has invalid type, must be a list of strings.".format(
module_name
)
Expand All @@ -438,7 +440,9 @@ def _get_valid_files_to_check(module_name: Union[List[str], str]) -> Generator[A
# Filter valid files to check
for item in module_name:
if not isinstance(item, str): # Issue errors for invalid types
print("No check run on file `{}`, with invalid type. Must be type: str.\n".format(item))
logging.error(
"No check run on file `{}`, with invalid type. Must be type: str.\n".format(item)
)
elif os.path.isdir(item):
yield item
elif not os.path.exists(os.path.expanduser(item)):
Expand All @@ -448,15 +452,15 @@ def _get_valid_files_to_check(module_name: Union[List[str], str]) -> Generator[A
if os.path.exists(filepath):
yield filepath
else:
print("Could not find the file called, `{}`\n".format(item))
logging.error("Could not find the file called, `{}`\n".format(item))
except ImportError:
print("Could not find the file called, `{}`\n".format(item))
logging.error("Could not find the file called, `{}`\n".format(item))
else:
yield item # Check other valid files.


def doc(msg_id: str) -> None:
"""Open a webpage explaining the error for the given message."""
msg_url = HELP_URL + "#" + msg_id.lower()
print("Opening {} in a browser.".format(msg_url))
logging.info("Opening {} in a browser.".format(msg_url))
webbrowser.open(msg_url)
6 changes: 3 additions & 3 deletions python_ta/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Within the config submodule, this .py file encompasses functions responsible
for managing all configuration-related tasks.
"""

import logging
import os
import sys
from pathlib import Path
Expand Down Expand Up @@ -48,7 +48,7 @@ def override_config(linter: PyLinter, config_location: AnyStr) -> None:
try:
_, config_args = config_file_parser.parse_config_file(file_path=config_location)
except OSError as ex:
print(ex, file=sys.stderr)
logging.error(ex)
sys.exit(32)

# Override the config options by parsing the provided file.
Expand Down Expand Up @@ -76,7 +76,7 @@ def load_messages_config(path: str, default_path: str, use_pyta_error_messages:
try:
merge_from = toml.load(path)
except FileNotFoundError:
print(f"[WARNING] Could not find messages config file at {str(Path(path).resolve())}.")
logging.warning(f"Could not find messages config file at {str(Path(path).resolve())}. ")
Copy link
Contributor

Choose a reason for hiding this comment

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

Delete the space at the end of the string

merge_from = {}

if not use_pyta_error_messages:
Expand Down
4 changes: 2 additions & 2 deletions python_ta/contracts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
[(assertion, compiled, return_val_var_name)].
"""
import inspect
import logging
import sys
import typing
from types import CodeType, FunctionType, ModuleType
Expand Down Expand Up @@ -613,8 +614,7 @@ def _debug(msg: str) -> None:
"""
if not DEBUG_CONTRACTS:
return

print("[PyTA]", msg, file=sys.stderr)
logging.debug(msg)


def _set_invariants(klass: type) -> None:
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/pylint_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# pylint: disable=unbalanced-tuple-unpacking
5 changes: 5 additions & 0 deletions tests/fixtures/unicode_decode_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"�"
"�"
"foo"
"foo"
"�"
24 changes: 24 additions & 0 deletions tests/test_config/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
Test suite for checking whether configuration worked correctly with user-inputted configurations.
"""
import json
import logging
import os
from unittest.mock import mock_open, patch

import pylint.lint
Copy link
Contributor

Choose a reason for hiding this comment

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

You do not need both this and the other pylint import statement you added. The typical idiom is import pylint.lint as lint

import pytest
from pylint import lint

import python_ta
from python_ta.config import load_messages_config, override_config

TEST_CONFIG = {
"pyta-number-of-messages": 10,
Expand Down Expand Up @@ -229,6 +233,26 @@ def test_config_parse_error_has_no_snippet() -> None:
assert snippet == ""


def test_override_config_logging(caplog) -> None:
"""Testing that the OSError in override_config is logged correctly"""
path = "C:\\foo\\tests\\file_fixtures\\test_f0011.pylintrc"
linter = lint.PyLinter()

with pytest.raises(SystemExit):
override_config(linter, path)
assert caplog.records[0].levelname == "ERROR"
assert f"The config file {path} doesn't exist!" in caplog.text


@patch("python_ta.config.toml.load", side_effect=FileNotFoundError)
def test_load_messages_config_logging(_, caplog):
try:
load_messages_config("non_existent_file.toml", "default_file.toml", True)
except FileNotFoundError:
assert "Could not find messages config file at" in caplog.text
assert "WARNING" in [record.levelname for record in caplog.records]


def test_allow_pylint_comments() -> None:
"""Test that checks whether the allow-pylint-comments configuration option works as expected when it is
set to True
Expand Down
26 changes: 26 additions & 0 deletions tests/test_contracts/test_contracts_debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from python_ta import contracts

contracts.DEBUG_CONTRACTS = True
from python_ta.contracts import check_contracts


def test_contracts_debug(caplog) -> None:
"""Test to see if _debug method is logging messages correctly"""
Copy link
Contributor

Choose a reason for hiding this comment

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

_debug is a function, but not a method


@check_contracts
def divide(x: int, y: int) -> int:
"""Return x // y.

Preconditions:
- invalid precondition
"""
return x // y

divide(6, 2)

for record in caplog.records:
assert record.levelname == "DEBUG"
assert (
"Warning: precondition invalid precondition could not be parsed as a valid Python expression"
in caplog.text
)
108 changes: 108 additions & 0 deletions tests/test_init_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""Tests for top level __init__.py logging functionality in pyta"""
import os
import tokenize
from unittest.mock import patch

import python_ta
from python_ta import _get_valid_files_to_check, _verify_pre_check


def test_check_log(caplog) -> None:
"""Testing logging in _check function when no exception is thrown"""
expected_messages = [
"was checked using the configuration file:",
"was checked using the messages-config file:",
]

python_ta._check()
for i in range(2):
assert caplog.records[i].levelname == "INFO"
assert expected_messages[i] in caplog.records[i].msg


@patch("python_ta._get_valid_files_to_check", side_effect=Exception("Testing"))
def test_check_exception_log(_, caplog) -> None:
"""Testing logging in _check function when exception is thrown"""
try:
python_ta._check()
except Exception:
expected_logs = [
"Unexpected error encountered! Please report this to your instructor (and attach the code that caused the error).",
'Error message: "Testing"',
]

for i in range(2):
assert caplog.records[i].levelname == "ERROR"
assert expected_logs[i] in caplog.records[i].msg


def test_pre_check_log_pylint_comment(caplog) -> None:
"""Testing logging in _verify_pre_check function when checking for pyling comment"""
path = os.path.join(os.path.dirname(__file__), "fixtures", "pylint_comment.py")
_verify_pre_check(path, False)
assert f'String "pylint:" found in comment. No check run on file `{path}' in caplog.text
assert "ERROR" == caplog.records[0].levelname


@patch("python_ta.tokenize.open", side_effect=IndentationError)
def test_pre_check_log_indentation_error(_, caplog) -> None:
"""Testing logging in _verify_pre_check function IndentationError catch block"""
# Don't need a valid file path since patching error into open function
_verify_pre_check("", False)
assert "python_ta could not check your code due to an indentation error at line" in caplog.text
assert "ERROR" == caplog.records[0].levelname


@patch("python_ta.tokenize.open", side_effect=tokenize.TokenError)
def test_pre_check_log_token_error(_, caplog) -> None:
"""Testing logging in _verify_pre_check function TokenError catch block"""
# Don't need a valid file path since patching error into open function
_verify_pre_check("", False)
assert "python_ta could not check your code due to a syntax error in your file." in caplog.text
assert "ERROR" == caplog.records[0].levelname


@patch("python_ta.tokenize.open", side_effect=UnicodeDecodeError("", b"", 0, 0, ""))
def test_pre_check_log_pylint_unicode_error(_, caplog) -> None:
"""Testing logging in _verify_pre_check function UnicodeDecodeError catch block"""
expected_logs = [
"python_ta could not check your code due to an invalid character. Please check the following lines in your file and all characters that are marked with a �.",
' Line 1: "�"\n',
' Line 2: "�"\n',
' Line 5: "�"',
]

path = os.path.join(os.path.dirname(__file__), "fixtures", "unicode_decode_error.py")
_verify_pre_check(path, False)

for i in range(len(expected_logs)):
assert expected_logs[i] == caplog.records[i].msg
assert "ERROR" == caplog.records[i].levelname


def test_get_valid_files_to_check(caplog) -> None:
"""Testing logging in _get_valid_files_to_check function"""
expected_logs = [
"No checks run. Input to check, `{'examples/nodes/assign'}`, has invalid type, must be a list of strings.",
"No check run on file `0`, with invalid type. Must be type: str.",
"Could not find the file called, `foo`",
]

# Iterating through generators to produce errors
[_ for _ in _get_valid_files_to_check(module_name={"examples/nodes/assign"})]
Copy link
Contributor

Choose a reason for hiding this comment

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

For a generator, you can execute until the first yield by calling next, e.g. next(_get_valid_files_to_check(...))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Neat. I didn't know about that. I also just looked it up, I could also just pass the generator function into tuple(). In this case, would it be better to use a while loop and next, or keep the code minimal and just use the tuple() constructor and "one-line" it?

Copy link
Contributor

Choose a reason for hiding this comment

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

@merrickliu888 sorry I missed this! Using tuple was totally fine. :)

[_ for _ in _get_valid_files_to_check(module_name=[0])]
[_ for _ in _get_valid_files_to_check(module_name="foo")]

for i in range(len(expected_logs)):
assert caplog.records[i].levelname == "ERROR"
assert expected_logs[i] in caplog.records[i].msg


def test_doc_log(caplog) -> None:
"""Testing logging in doc function"""
python_ta.doc("E0602")
assert caplog.records[0].levelname == "INFO"
assert (
"Opening http://www.cs.toronto.edu/~david/pyta/checkers/index.html#e0602 in a browser."
in caplog.text
)
14 changes: 14 additions & 0 deletions tests/test_init_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Test for __init__.py system version check"""
import importlib
import sys

import python_ta


def test_sys_version_log(caplog, monkeypatch) -> None:
"""Testing if message logged when system version is too low is correct"""
monkeypatch.setattr(sys, "version_info", (2, 6, 0))
importlib.reload(python_ta) # Necessary due to python's import not actually reimporting package

assert caplog.records[0].levelname == "WARNING"
assert "You need Python 3.7 or later to run PythonTA." in caplog.text
Loading