Notice Changers

This plugin providers some helpers for modifying notices so that the api of the notices themselves remains small.

These helpers are split into high level helpers available in pytest_typing_runner.notices and low level helpers available in pytest_typing_runner.notice_changers.

High level changers

class pytest_typing_runner.notices.AddRevealedTypes(*, name: str, revealed: Sequence[str], replace: bool = False)

Used to add revealed type notices to a specific line

from pytest_typing_runner import notices, protocols


file_notices: protocols.FileNotices = ...
assert [n.display() for n in file_notices.notices_at_line(1)] == [
    'severity=note:: Revealed type is "one"',
    'severity=error[arg-type]:: an error',
    'severity=note:: a note',
]
assert file_notices.get_line_number("line_name") == 1

changed = notices.AddRevealedTypes(
    name="line_name",
    revealed=["two", "three"],
)(file_notices)
assert [n.display() for n in file_notices.notices_at_line(1)] == [
    'severity=note:: Revealed type is "one"',
    'severity=error[arg-type]:: an error',
    'severity=note:: a note',
    'severity=note:: Revealed type is "two"',
    'severity=note:: Revealed type is "three"',
]

Where existing revealed notes can be removed. For example, continuing the code example:

changed = notices.AddRevealedTypes(
    name="line_name",
    revealed=["two", "three"],
    replace=True
)(file_notices)
assert [n.display() for n in file_notices.notices_at_line(1)] == [
    'severity=error[arg-type]:: an error',
    'severity=note:: a note',
    'severity=note:: Revealed type is "two"',
    'severity=note:: Revealed type is "three"',
]

Note

When there are multiple items in revealed, only one notice is appended where the different notes are in a single multiline string for the msg of that one notice. The default implementation for comparing notices already knows how to split these into multiple notices.

Parameters:
  • name – The name of the line to change. The name must already be registered

  • revealed – A sequence of strings to add reveal messages for. Each message is wrapped such that if the string is ‘X’ the result is ‘Revealed type is “X”’

  • replace – Defaults to False. When True any existing reveal notes will be removed before new ones are added.

__call__(notices: FileNotices) FileNotices

Peforms the transformation

Parameters:

notices – The file notices to change

Raises:

MissingNotices – When the specified name isn’t registered with the file notices.

Returns:

Copy of the file notices with additional reveal notes, where existing reveal notes have been removed if replace is True

class pytest_typing_runner.notices.AddErrors(*, name: str, errors: Sequence[tuple[str, str | NoticeMsg]], replace: bool = False)

Used to add error notices to a specific line

from pytest_typing_runner import notices, protocols


file_notices: protocols.FileNotices = ...
assert [n.display() for n in file_notices.notices_at_line(1)] == [
    'severity=note:: Revealed type is "one"',
    'severity=error[arg-type]:: an error',
    'severity=note:: a note',
]
assert file_notices.get_line_number("line_name") == 1

changed = notices.AddErrors(
    name="line_name",
    errors=[("misc", "error two"), ("assignment", "error three")],
)(file_notices)
assert [n.display() for n in file_notices.notices_at_line(1)] == [
    'severity=note:: Revealed type is "one"',
    'severity=error[arg-type]:: an error',
    'severity=note:: a note',
    'severity=error[misc]:: error two',
    'severity=error[assignment]:: error three',
]

Where existing errors can be removed. For example, continuing the code example:

changed = notices.AddErrors(
    name="line_name",
    errors=[("misc", "error two"), ("assignment", "error three")],
    replace=True,
)(file_notices)
assert [n.display() for n in file_notices.notices_at_line(1)] == [
    'severity=note:: Revealed type is "one"',
    'severity=note:: a note',
    'severity=error[misc]:: error two',
    'severity=error[assignment]:: error three',
]

Note

Unlike AddRevealedTypes every entry in errors becomes it’s own notice.

Parameters:
  • name – The name of the line to change. The name must already be registered

  • errors – A sequence of two string tuples where the first string is the error type and the second string is the error message.

  • replace – Defaults to False. When True any existing error notices will be removed before new ones are added.

__call__(notices: FileNotices) FileNotices

Peforms the transformation

Parameters:

notices – The file notices to change

Raises:

MissingNotices – When the specified name isn’t registered with the file notices.

Returns:

Copy of the file notices with additional error notices, where existing error notices have been removed if replace is True

class pytest_typing_runner.notices.AddNotes(*, name: str, notes: Sequence[str | NoticeMsg], replace: bool = False, keep_reveals: bool = True)

Used to add note notices to a specific line

from pytest_typing_runner import notices, protocols


file_notices: protocols.FileNotices = ...
assert [n.display() for n in file_notices.notices_at_line(1)] == [
    'severity=note:: Revealed type is "one"',
    'severity=error[arg-type]:: an error',
    'severity=note:: a note',
]
assert file_notices.get_line_number("line_name") == 1

changed = notices.AddNotes(
    name="line_name",
    notes=["two", "three"],
)(file_notices)
assert [n.display() for n in file_notices.notices_at_line(1)] == [
    'severity=note:: Revealed type is "one"',
    'severity=error[arg-type]:: an error',
    'severity=note:: a note',
    'severity=note:: two',
    'severity=note:: three',
]

Where existing notes can be removed. For example, continuing the code example:

changed = notices.AddNotes(
    name="line_name",
    notes=["two", "three"],
    replace=True,
)(file_notices)
assert [n.display() for n in file_notices.notices_at_line(1)] == [
    'severity=error[arg-type]:: an error',
    'severity=note:: two',
    'severity=note:: three',
]

And existing reveal notes can be left alone, continuing the code example:

changed = notices.AddNotes(
    name="line_name",
    notes=["two", "three"],
    replace=True,
    keep_reveals=True,
)(file_notices)
assert [n.display() for n in file_notices.notices_at_line(1)] == [
    'severity=note:: Revealed type is "one"',
    'severity=error[arg-type]:: an error',
    'severity=note:: two',
    'severity=note:: three',
]

Note

Like :class:AddRevealedTypes: only one notice is append to the end where the message is a multiline string with each note on it’s own line

Parameters:
  • name – The name of the line to change. The name must already be registered

  • notes – A sequence of strings representing each note to add

  • replace – Defaults to False. When True any existing notes removed before new ones are added.

  • keep_reveals – Defaults to True. When replace is True and this is True then notes that are type reveals will not be removed.

__call__(notices: FileNotices) FileNotices

Peforms the transformation

Parameters:

notices – The file notices to change

Raises:

MissingNotices – When the specified name isn’t registered with the file notices.

Returns:

Copy of the file notices with additional notes, where existing notes are removed depending on the values of replace and keep_reveals.

class pytest_typing_runner.notices.RemoveFromRevealedType(*, name: str, remove: str, must_exist: bool = True)

Used to remove some specific string from existing reveal notes.

from pytest_typing_runner import notices, protocols


file_notices: protocols.FileNotices = ...
assert [n.display() for n in file_notices.notices_at_line(1)] == [
    'severity=note:: Revealed type is "some specific message"',
    'severity=error[arg-type]:: an error',
    'severity=note:: a specific note',
]
assert file_notices.get_line_number("line_name") == 1

changed = notices.RemoveFromRevealedType(
    name="line_name",
    remove="specific",
)(file_notices)
assert [n.display() for n in file_notices.notices_at_line(1)] == [
    'severity=note:: Revealed type is "some  message"',
    'severity=error[arg-type]:: an error',
    'severity=note:: a specific note',
]
Parameters:
  • name – The name of the line to change. The name must already be registered

  • remove – The string to remove from all reveal notes that are found.

  • must_exist – Defaults to True. When True and no reveal notes are found with the specified replace string, then MissingNotices will be raised.

__call__(notices: FileNotices) FileNotices

Peforms the transformation

Parameters:

notices – The file notices to change

Raises:
  • MissingNotices – When the specified name isn’t registered with the file notices.

  • MissingNotices – When must_exist is True and no reveal notes with the replace string are found

Returns:

Copy of the file notices where all revealed notes at the specified line have the replace string removed from their msg.

Low level changers

exception pytest_typing_runner.notice_changers.MissingNotices(*, location: Path, name: str | int | None = None, line_number: int | None = None)

Raised when notices are expected to be somewhere they are not

Parameters:
  • location – The file the notices were expected to be in

  • name – Optional name of the line notices expected to be on

  • line_number – Optional line number the notices were expected to be on

class pytest_typing_runner.notice_changers.FirstMatchOnly(*, change: ProgramNoticeChanger)

Used to change a ProgramNotice such that only the first time this is used a change will be made:

from pytest_typing_runner import notice_changers, protocols


changer = notice_changers.FirstMatchOnly(
    change = lambda notice: notice.clone(msg="changed!")
)

existing_notice: protocols.ProgramNotice = ...
changed_notice = changer(existing_notice)
assert changed_notice is not existing_notice

other_existing_notice: protocols.ProgramNotice = ...
# subsequent changes will pass through existing notice
changed_other_notice = changer(other_existing_notice)
assert changed_other_notice is other_existing_notice
Parameters:

change – A function that takes in a program notice and returns either None if the notice should be removed, or a program notice to replace it.

__call__(notice: ProgramNotice, /) ProgramNotice | None

Perform the transformation

Parameters:

notice – The notice to change

Returns:

Clone of the program notice with changes, or None if the notice should be deleted

class pytest_typing_runner.notice_changers.AppendToLine(*, notices_maker: Callable[[LineNotices], Sequence[ProgramNotice | None]])

Used to append notices to a line notices.

from pytest_typing_runner import notice_changer, protocols


existing_line_notices: protocols.LineNotices = ...
assert list(existing_line_notices) == [_existing_notice1, _existing_notice2]

changer = notice_changer.AppendToLine(
    notices_maker = lambda ln: [_new_notice1, new_notice2]
)

changed = changer(existing_line_notices)
assert list(existing_line_notices) == [_existing_notice1, _existing_notice2, _new_notice1, _new_notice2]
Parameters:

notices_maker – Callable that takes the line notices being changed and returns a sequence of program notice objects to add to that line notices. Any None values in the sequence will be ignored.

__call__(notices: LineNotices, /) LineNotices

Perform the transformation

Parameters:

notices – The line notices to change

Returns:

A copy of the passed in notices with the additional notices appended. This changer never returns None.

class pytest_typing_runner.notice_changers.ModifyLatestMatch(*, must_exist: bool = False, allow_empty: bool = False, change: ProgramNoticeChanger, matcher: Callable[[ProgramNotice], bool])

Used to match against a particular notice and change the first one that matches where the matching happens from the end of the notices back.

So if the line notices has [n1, n2, n3] then it will try to match n3 then n2, then n1. Once a notice has been changed, the rest will not be changed.

By default if no notice matches then a new notice is added to the end and run through the change argument.

from pytest_typing_runner import protocols, notice_changers


line_notices: protocols.LineNotices = ...
assert [n.msg for n in list(line_notices)] == ["n1", "s1", "n2", "s2", "n3"]

changer = notice_changers.ModifyLatestMatch(
    change = lambda notice: notice.clone(msg="changed!"),
    matcher = lambda notice: notice.msg.startswith("s")
)

changed = changer(line_notices)
assert [n.msg for n in (list(changed) or [])] == ["n1", "s1", "n2", "changed!", "n3"]
Parameters:
  • must_exist – Defaults to False, if passed in as True then an exception will be raised if no notice matches

  • allow_empty – Defaults to False, if passed in as True then this changer will never return None. Otherwise if there are no notices in the line notices after the change, then None will be returned to indicate the notices should be deleted.

  • change – A function to be given the notice to change. Note that if must_exist is False and no notice matches, then it will be given a notice with a default “note” severity and empty message.

  • matcher – A function given each notice from the back to the front till True is returned to indicate a notice that should be changed

__call__(notices: LineNotices) LineNotices | None

Perform the transformation

Parameters:

notices – The line notices to change

Raises:

MissingNotices – if no notice matches and must_exist is True

Returns:

A copy of the line notices with the changed notice, or None if allow_empty is False and no notices are left after the change.

class pytest_typing_runner.notice_changers.ModifyLine(*, name_or_line: str | int, name_must_exist: bool = True, line_must_exist: bool = True, change: LineNoticesChanger)

Used to modify a line notices at a specific line in a file notices.

from pytest_typing_runner import notice_changers, protocols


file_notices: protocols.FileNotices = ...
assert file_notices.notices_at_line(5) is None

changer = notice_changers.ModifyLine(
    name_or_line=5,
    change=lambda line_notices: line_notices.set_notices([_new_notice1, _new_notice2])
)

changed = changer(file_notices)
assert list(changed.notices_at_line(5) or []) == [_new_notice1, _new_notice2]

This also works with named lines:

from pytest_typing_runner import notice_changers, protocols


file_notices: protocols.FileNotices = ...
file_notices = file_notices.set_name("one", 5)
assert file_notices.notices_at_line(5) is None

changer = notice_changers.ModifyLine(
    name_or_line="one",
    change=lambda line_notices: line_notices.set_notices([_new_notice1, _new_notice2])
)

changed = changer(file_notices)
assert list(changed.notices_at_line(5) or []) == [_new_notice1, _new_notice2]
Parameters:
  • name_or_line – Either a string used as a name, or an integer representing the specific line.

  • name_must_exist – Defaults to True. Will raise MissingNotices if True and name_or_line is a string that isn’t registered on the file notices.

  • line_must_exist – Defaults to True. Will raise MissingNotices if True and the resolved line number from name_or_line doesn’t have an existing line notices.

  • change – A function given a line notices to return a new line notices to give to the file notices. If this function returns None then the file notices will be told to not have notices for the resolved line number. If the name_must_exist and line_must_exist flags are False and the resolved line number doesn’t have an existing line notices, a new one will be generated to pass into change

__call__(notices: FileNotices) FileNotices

Perform the transformation

Parameters:

notices – The file notices to change

Raises:
  • MissingNotices – if name_must_exist is True and name_or_line is a string and not a registered name

  • MissingNotices – if line_must_exist is True and the resolved line number doesn’t already have line notices on the file notices.

Returns:

A copy of the file notices with changed line notices for the resolved line number.

class pytest_typing_runner.notice_changers.ModifyFile(*, location: Path, must_exist: bool = True, change: FileNoticesChanger)

Used to modify the notices for a particular location on a program notices.

from pytest_typing_runner import notice_changers, protocols
import pathlib


location = pathlib.Path(...)

program_notices: protocols.ProgramNotices = ...
assert list(program_notices.notices_at_location(location) or []) == [
    _existing_line_1_notice,
    _existing_line_2_notice,
]

changer = notice_changers.ModifyFile(
    location=location,
    change=lambda file_notices: file_notices.set_lines(
        {
            5: file_notices.generate_notices_for_line(5).set_notices([_new_line_5_notice])
        }
    )
)

changed = changer(program_notices)
assert list(program_notices.notices_at_location(location) or []) == [
    _existing_line_1_notice,
    _existing_line_2_notice,
    _new_line_5_notice,
]
Parameters:
  • location – The location to modify

  • must_exist – Defaults to True. When True and the location isn’t already in the program notices a MissingNotices will be raised.

  • change – A function that takes a file notices to change. One will be generated if the location isn’t already in the program notices and must_exist is False.

__call__(notices: ProgramNotices) ProgramNotices

Perform the transformation

Parameters:

notices – The program notices to change

Raises:

MissingNotices – if must_exist is True and the location isn’t already in the program notices.

Returns:

A copy of the program notices with changed file notices for the specified location.

class pytest_typing_runner.notice_changers.BulkAdd(*, root_dir: Path, add: Mapping[str, Mapping[int | tuple[int, str], Sequence[str | NoticeMsg | tuple[Severity, str]]]])

Used to bulk add notices to a program notices.

from pytest_typing_runner import notice_changers, protocols
import pathlib


root_dir = pathlib.Path(...)

program_notices: protocols.ProgramNotices = ...

changer = notice_changers.BulkAdd(
    root_dir=root_dir,
    add={
        "one": {
            1: [
                "a note",
                (notices.ErrorSeverity("arg-type"), "an error"),
            ],
            (20, "name"): [...],
        },
        "two/three": {...},
    },
)

program_notices = changer(program_notices)
# and now we have program notices for:
# location=(root_dir / "one") | line_number=1 | severity=note | msg="a note"
# location=(root_dir / "one") | line_number=1 | severity=error(arg-type) | msg="an error"
# location=(root_dir / "one") | line_number=20 | ...
# location=(root_dir / "two" / "three") | ...
#
# And line 20 will have the name "name"
Parameters:
  • root_dir – The path all locations are relative to

  • add

    A Mapping of path to Mapping of line number to sequence of notices.

    Where the key for the line number is either an integer or a tuple of (line_number, line_name) for registering that name to that line number in that location.

    Where the notices are either a string indicating a note severity notice or a tuple of (severity, msg).

__call__(notices: ProgramNotices) ProgramNotices

Add the specified notices

Parameters:

notices – The program notices to change

Returns:

A copy of the program notices with additional notices.