diff --git a/ruff.toml b/ruff.toml index f6fd8b7..0fa381a 100644 --- a/ruff.toml +++ b/ruff.toml @@ -25,7 +25,7 @@ lint.extend-select = [ "TRY", # various exception handling rules "UP", # detect deprecated python stdlib stuff "FA", # suggest using from __future__ import annotations - "PTH", # pathlib migration + # "PTH", # pathlib migration # FIXME do later.. a bit overwhelming "ARG", # unused argument checks "A", # builtin shadowing "G", # logging stuff diff --git a/src/orgparse/__init__.py b/src/orgparse/__init__.py index 416a3b7..d699f4f 100644 --- a/src/orgparse/__init__.py +++ b/src/orgparse/__init__.py @@ -106,14 +106,13 @@ """ # [[[end]]] -from io import IOBase +from collections.abc import Iterable from pathlib import Path -from typing import Iterable, Union, Optional, TextIO +from typing import Optional, TextIO, Union +from .node import OrgEnv, OrgNode, parse_lines # todo basenode?? -from .node import parse_lines, OrgEnv, OrgNode # todo basenode?? - -__all__ = ["load", "loads", "loadi"] +__all__ = ["load", "loadi", "loads"] def load(path: Union[str, Path, TextIO], env: Optional[OrgEnv] = None) -> OrgNode: diff --git a/src/orgparse/date.py b/src/orgparse/date.py index dd407b7..ccaa5c3 100644 --- a/src/orgparse/date.py +++ b/src/orgparse/date.py @@ -1,22 +1,25 @@ +from __future__ import annotations + import datetime import re -from typing import Union, Tuple, Optional, List +from datetime import timedelta +from typing import Optional, Union DateIsh = Union[datetime.date, datetime.datetime] -def total_seconds(td): +def total_seconds(td: timedelta) -> float: """Equivalent to `datetime.timedelta.total_seconds`.""" return float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10 ** 6 -def total_minutes(td): +def total_minutes(td: timedelta) -> float: """Alias for ``total_seconds(td) / 60``.""" return total_seconds(td) / 60 -def gene_timestamp_regex(brtype, prefix=None, nocookie=False): +def gene_timestamp_regex(brtype: str, prefix: str | None = None, *, nocookie: bool = False) -> str: """ Generate timestamp regex for active/inactive/nobrace brace type @@ -84,15 +87,15 @@ def gene_timestamp_regex(brtype, prefix=None, nocookie=False): elif brtype == 'nobrace': (bo, bc) = ('', '') else: - raise ValueError("brtype='{0!r}' is invalid".format(brtype)) + raise ValueError(f"brtype='{brtype!r}' is invalid") if brtype == 'nobrace': ignore = r'[\s\w]' else: - ignore = '[^{bc}]'.format(bc=bc) + ignore = f'[^{bc}]' if prefix is None: - prefix = '{0}_'.format(brtype) + prefix = f'{brtype}_' regex_date_time = r""" (?P<{prefix}year>\d{{4}}) - @@ -133,7 +136,7 @@ def gene_timestamp_regex(brtype, prefix=None, nocookie=False): return regex.format(prefix=prefix, ignore=ignore) -def date_time_format(date) -> str: +def date_time_format(date: DateIsh) -> str: """ Format a date or datetime in default org format @@ -165,7 +168,10 @@ def is_same_day(date0, date1) -> bool: re.VERBOSE) -class OrgDate(object): +_Repeater = tuple[str, int, str] + + +class OrgDate: _active_default = True """ @@ -184,8 +190,14 @@ class OrgDate(object): """ _allow_short_range = True - def __init__(self, start, end=None, active=None, repeater=None, - warning=None): + def __init__( + self, + start, + end=None, + active: bool | None = None, + repeater: _Repeater | None = None, + warning: _Repeater | None = None, + ) -> None: """ Create :class:`OrgDate` object @@ -242,21 +254,23 @@ def _to_date(date) -> DateIsh: raise ValueError( "Automatic conversion to the datetime object " "requires at least 3 elements in the tuple. " - "Only {0} elements are in the given tuple '{1}'." - .format(len(date), date)) + f"Only {len(date)} elements are in the given tuple '{date}'." + ) elif isinstance(date, (int, float)): return datetime.datetime.fromtimestamp(date) else: return date @staticmethod - def _date_to_tuple(date): + def _date_to_tuple(date: DateIsh) -> tuple[int, ...]: if isinstance(date, datetime.datetime): return tuple(date.timetuple()[:6]) elif isinstance(date, datetime.date): return tuple(date.timetuple()[:3]) + else: + raise RuntimeError(f"can't happen: {date}") - def __repr__(self): + def __repr__(self) -> str: args = [ self.__class__.__name__, self._date_to_tuple(self.start), @@ -269,9 +283,9 @@ def __repr__(self): args.pop() if len(args) > 3 and args[3] is None: args[3] = self._active_default - return '{0}({1})'.format(args[0], ', '.join(map(repr, args[1:]))) + return '{}({})'.format(args[0], ', '.join(map(repr, args[1:]))) - def __str__(self): + def __str__(self) -> str: fence = ("<", ">") if self.is_active() else ("[", "]") start = date_time_format(self.start) @@ -279,26 +293,26 @@ def __str__(self): if self.has_end(): if self._allow_short_range and is_same_day(self.start, self.end): - start += "--%s" % self.end.strftime("%H:%M") + start += "--{}".format(self.end.strftime("%H:%M")) else: end = date_time_format(self.end) - if self._repeater: - start += " %s%d%s" % self._repeater - if self._warning: - start += " %s%d%s" % self._warning - ret = "%s%s%s" % (fence[0], start, fence[1]) + if self._repeater is not None: + (x, y, z) = self._repeater + start += f" {x}{y}{z}" + if self._warning is not None: + (x, y, z) = self._warning + start += f" {x}{y}{z}" + ret = f"{fence[0]}{start}{fence[1]}" if end: - ret += "--%s%s%s" % (fence[0], end, fence[1]) + ret += f"--{fence[0]}{end}{fence[1]}" return ret - def __nonzero__(self): + def __bool__(self) -> bool: return bool(self._start) - __bool__ = __nonzero__ # PY3 - - def __eq__(self, other): + def __eq__(self, other) -> bool: if (isinstance(other, OrgDate) and self._start is None and other._start is None): @@ -309,7 +323,7 @@ def __eq__(self, other): self._active == other._active) @property - def start(self): + def start(self) -> DateIsh: """ Get date or datetime object @@ -322,7 +336,7 @@ def start(self): return self._start @property - def end(self): + def end(self) -> DateIsh: """ Get date or datetime object @@ -404,11 +418,11 @@ def _as_datetime(date) -> datetime.datetime: return date @staticmethod - def _daterange_from_groupdict(dct, prefix='') -> Tuple[List, Optional[List]]: + def _daterange_from_groupdict(dct, prefix='') -> tuple[list, Optional[list]]: start_keys = ['year', 'month', 'day', 'hour' , 'min'] end_keys = ['year', 'month', 'day', 'end_hour', 'end_min'] start_range = list(map(int, filter(None, (dct[prefix + k] for k in start_keys)))) - end_range: Optional[List] + end_range: Optional[list] end_range = list(map(int, filter(None, (dct[prefix + k] for k in end_keys)))) if len(end_range) < len(end_keys): end_range = None @@ -419,7 +433,7 @@ def _datetuple_from_groupdict(cls, dct, prefix=''): return cls._daterange_from_groupdict(dct, prefix=prefix)[0] @classmethod - def list_from_str(cls, string: str) -> List['OrgDate']: + def list_from_str(cls, string: str) -> list[OrgDate]: """ Parse string and return a list of :class:`OrgDate` objects @@ -447,8 +461,8 @@ def list_from_str(cls, string: str) -> List['OrgDate']: prefix = 'inactive_' active = False rangedash = '--[' - repeater: Optional[Tuple[str, int, str]] = None - warning: Optional[Tuple[str, int, str]] = None + repeater: Optional[tuple[str, int, str]] = None + warning: Optional[tuple[str, int, str]] = None if mdict[prefix + 'repeatpre'] is not None: keys = [prefix + 'repeat' + suffix for suffix in cookie_suffix] values = [mdict[k] for k in keys] @@ -471,12 +485,12 @@ def list_from_str(cls, string: str) -> List['OrgDate']: odate = cls( *cls._daterange_from_groupdict(mdict, prefix), active=active, repeater=repeater, warning=warning) - return [odate] + cls.list_from_str(rest) + return [odate, *cls.list_from_str(rest)] else: return [] @classmethod - def from_str(cls, string): + def from_str(cls, string: str) -> OrgDate: """ Parse string and return an :class:`OrgDate` objects. @@ -500,7 +514,7 @@ def from_str(cls, string): def compile_sdc_re(sdctype): brtype = 'inactive' if sdctype == 'CLOSED' else 'active' return re.compile( - r'^(?!\#).*{0}:\s+{1}'.format( + r'^(?!\#).*{}:\s+{}'.format( sdctype, gene_timestamp_regex(brtype, prefix='', nocookie=True)), re.VERBOSE) @@ -528,8 +542,8 @@ def from_str(cls, string): end_dict.update({'hour': end_hour, 'min': end_min}) end = cls._datetuple_from_groupdict(end_dict) cookie_suffix = ['pre', 'num', 'dwmy'] - repeater: Optional[Tuple[str, int, str]] = None - warning: Optional[Tuple[str, int, str]] = None + repeater: Optional[tuple[str, int, str]] = None + warning: Optional[tuple[str, int, str]] = None prefix = '' if mdict[prefix + 'repeatpre'] is not None: keys = [prefix + 'repeat' + suffix for suffix in cookie_suffix] @@ -588,7 +602,7 @@ def __init__(self, start, end=None, duration=None, active=None): """ Create OrgDateClock object """ - super(OrgDateClock, self).__init__(start, end, active=active) + super().__init__(start, end, active=active) self._duration = duration @property @@ -625,7 +639,7 @@ def is_duration_consistent(self): self._duration == total_minutes(self.duration)) @classmethod - def from_str(cls, line: str) -> 'OrgDateClock': + def from_str(cls, line: str) -> OrgDateClock: """ Get CLOCK from given string. @@ -674,26 +688,26 @@ class OrgDateRepeatedTask(OrgDate): _active_default = False - def __init__(self, start, before, after, active=None): - super(OrgDateRepeatedTask, self).__init__(start, active=active) + def __init__(self, start, before: str, after: str, active=None) -> None: + super().__init__(start, active=active) self._before = before self._after = after - def __repr__(self): - args = [self._date_to_tuple(self.start), self.before, self.after] + def __repr__(self) -> str: + args: list = [self._date_to_tuple(self.start), self.before, self.after] if self._active is not self._active_default: args.append(self._active) - return '{0}({1})'.format( + return '{}({})'.format( self.__class__.__name__, ', '.join(map(repr, args))) - def __eq__(self, other): - return super(OrgDateRepeatedTask, self).__eq__(other) and \ + def __eq__(self, other) -> bool: + return super().__eq__(other) and \ isinstance(other, self.__class__) and \ self._before == other._before and \ self._after == other._after @property - def before(self): + def before(self) -> str: """ The state of task before marked as done. @@ -705,7 +719,7 @@ def before(self): return self._before @property - def after(self): + def after(self) -> str: """ The state of task after marked as done. diff --git a/src/orgparse/extra.py b/src/orgparse/extra.py index 5fefcd6..c720200 100644 --- a/src/orgparse/extra.py +++ b/src/orgparse/extra.py @@ -1,6 +1,8 @@ -import re -from typing import List, Sequence, Dict, Iterator, Iterable, Union, Optional, Type +from __future__ import annotations +import re +from collections.abc import Iterator, Sequence +from typing import Optional, Union RE_TABLE_SEPARATOR = re.compile(r'\s*\|(\-+\+)*\-+\|') RE_TABLE_ROW = re.compile(r'\s*\|([^|]+)+\|') @@ -10,12 +12,12 @@ Row = Sequence[str] class Table: - def __init__(self, lines: List[str]) -> None: + def __init__(self, lines: list[str]) -> None: self._lines = lines @property def blocks(self) -> Iterator[Sequence[Row]]: - group: List[Row] = [] + group: list[Row] = [] first = True for r in self._pre_rows(): if r is None: @@ -49,7 +51,7 @@ def _pre_rows(self) -> Iterator[Optional[Row]]: # TODO use iparse helper? @property - def as_dicts(self) -> 'AsDictHelper': + def as_dicts(self) -> AsDictHelper: bl = list(self.blocks) if len(bl) != 2: raise RuntimeError('Need two-block table to non-ambiguously guess column names') @@ -69,9 +71,9 @@ def __init__(self, columns: Sequence[str], rows: Sequence[Row]) -> None: self.columns = columns self._rows = rows - def __iter__(self) -> Iterator[Dict[str, str]]: + def __iter__(self) -> Iterator[dict[str, str]]: for x in self._rows: - yield {k: v for k, v in zip(self.columns, x)} + yield dict(zip(self.columns, x)) class Gap: @@ -89,8 +91,8 @@ def to_rich_text(text: str) -> Iterator[Rich]: At the moment only tables are supported. ''' lines = text.splitlines(keepends=True) - group: List[str] = [] - last: Type[Rich] = Gap + group: list[str] = [] + last: type[Rich] = Gap def emit() -> Rich: nonlocal group, last if last is Gap: diff --git a/src/orgparse/node.py b/src/orgparse/node.py index 7ed1cdb..f6044a2 100644 --- a/src/orgparse/node.py +++ b/src/orgparse/node.py @@ -1,14 +1,30 @@ -import re -import itertools -from typing import List, Iterable, Iterator, Optional, Union, Tuple, cast, Dict, Set, Sequence, Any +from __future__ import annotations -from .date import OrgDate, OrgDateClock, OrgDateRepeatedTask, parse_sdc, OrgDateScheduled, OrgDateDeadline, OrgDateClosed +import itertools +import re +from collections.abc import Iterable, Iterator, Sequence +from typing import ( + Any, + Optional, + Union, + cast, +) + +from .date import ( + OrgDate, + OrgDateClock, + OrgDateClosed, + OrgDateDeadline, + OrgDateRepeatedTask, + OrgDateScheduled, + parse_sdc, +) +from .extra import Rich, to_rich_text from .inline import to_plain_text -from .extra import to_rich_text, Rich -def lines_to_chunks(lines: Iterable[str]) -> Iterable[List[str]]: - chunk: List[str] = [] +def lines_to_chunks(lines: Iterable[str]) -> Iterable[list[str]]: + chunk: list[str] = [] for l in lines: if RE_NODE_HEADER.search(l): yield chunk @@ -19,7 +35,7 @@ def lines_to_chunks(lines: Iterable[str]) -> Iterable[List[str]]: RE_NODE_HEADER = re.compile(r"^\*+ ") -def parse_heading_level(heading): +def parse_heading_level(heading: str) -> tuple[str, int] | None: """ Get star-stripped heading and its level @@ -32,14 +48,15 @@ def parse_heading_level(heading): >>> parse_heading_level('not heading') # None """ - match = RE_HEADING_STARS.search(heading) - if match: - return (match.group(2), len(match.group(1))) + m = RE_HEADING_STARS.search(heading) + if m is not None: + return (m.group(2), len(m.group(1))) + return None RE_HEADING_STARS = re.compile(r'^(\*+)\s+(.*?)\s*$') -def parse_heading_tags(heading: str) -> Tuple[str, List[str]]: +def parse_heading_tags(heading: str) -> tuple[str, list[str]]: """ Get first tags and heading without tags @@ -74,7 +91,7 @@ def parse_heading_tags(heading: str) -> Tuple[str, List[str]]: RE_HEADING_TAGS = re.compile(r'(.*?)\s*:([\w@:]+):\s*$') -def parse_heading_todos(heading: str, todo_candidates: List[str]) -> Tuple[str, Optional[str]]: +def parse_heading_todos(heading: str, todo_candidates: list[str]) -> tuple[str, Optional[str]]: """ Get TODO keyword and heading without TODO keyword. @@ -116,7 +133,7 @@ def parse_heading_priority(heading): RE_HEADING_PRIORITY = re.compile(r'^\s*\[#([A-Z0-9])\] ?(.*)$') PropertyValue = Union[str, int, float] -def parse_property(line: str) -> Tuple[Optional[str], Optional[PropertyValue]]: +def parse_property(line: str) -> tuple[Optional[str], Optional[PropertyValue]]: """ Get property from given string. @@ -219,7 +236,7 @@ def parse_duration_to_minutes_float(duration: str) -> float: return parse_duration_to_minutes_float(units_part) + parse_duration_to_minutes_float(hms_part) if RE_FLOAT.fullmatch(duration): return float(duration) - raise ValueError("Invalid duration format %s" % duration) + raise ValueError(f"Invalid duration format {duration}") # Conversion factor to minutes for a duration. ORG_DURATION_UNITS = { @@ -231,7 +248,7 @@ def parse_duration_to_minutes_float(duration: str) -> float: "y": 60 * 24 * 365.25, } # Regexp matching for all units. -ORG_DURATION_UNITS_RE = r'(%s)' % r'|'.join(ORG_DURATION_UNITS.keys()) +ORG_DURATION_UNITS_RE = r'({})'.format(r'|'.join(ORG_DURATION_UNITS.keys())) # Regexp matching a duration expressed with H:MM or H:MM:SS format. # Hours can use any number of digits. ORG_DURATION_H_MM_RE = r'[ \t]*[0-9]+(?::[0-9]{2}){1,2}[ \t]*' @@ -244,13 +261,13 @@ def parse_duration_to_minutes_float(duration: str) -> float: RE_ORG_DURATION_UNIT = re.compile(ORG_DURATION_UNIT_RE) # Regexp matching a duration expressed with units. # Allowed units are defined in ORG_DURATION_UNITS. -ORG_DURATION_FULL_RE = r'(?:[ \t]*%s)+[ \t]*' % ORG_DURATION_UNIT_RE +ORG_DURATION_FULL_RE = rf'(?:[ \t]*{ORG_DURATION_UNIT_RE})+[ \t]*' RE_ORG_DURATION_FULL = re.compile(ORG_DURATION_FULL_RE) # Regexp matching a duration expressed with units and H:MM or H:MM:SS format. # Allowed units are defined in ORG_DURATION_UNITS. # Match group A contains units part. # Match group B contains H:MM or H:MM:SS part. -ORG_DURATION_MIXED_RE = r'(?P([ \t]*%s)+)[ \t]*(?P[0-9]+(?::[0-9][0-9]){1,2})[ \t]*' % ORG_DURATION_UNIT_RE +ORG_DURATION_MIXED_RE = rf'(?P([ \t]*{ORG_DURATION_UNIT_RE})+)[ \t]*(?P[0-9]+(?::[0-9][0-9]){{1,2}})[ \t]*' RE_ORG_DURATION_MIXED = re.compile(ORG_DURATION_MIXED_RE) # Regexp matching float numbers. RE_FLOAT = re.compile(r'[0-9]+([.][0-9]*)?') @@ -311,22 +328,30 @@ def parse_seq_todo(line): list(map(strip_fast_access_key, dones.split()))) -class OrgEnv(object): +class OrgEnv: """ Information global to the file (e.g, TODO keywords). """ - def __init__(self, todos=['TODO'], dones=['DONE'], - filename=''): + def __init__( + self, + todos: Sequence[str] | None = None, + dones: Sequence[str] | None = None, + filename: str = '', + ) -> None: + if dones is None: + dones = ['DONE'] + if todos is None: + todos = ['TODO'] self._todos = list(todos) self._dones = list(dones) self._todo_not_specified_in_comment = True self._filename = filename - self._nodes = [] + self._nodes: list[OrgBaseNode] = [] @property - def nodes(self): + def nodes(self) -> list[OrgBaseNode]: """ A list of org nodes. @@ -392,15 +417,12 @@ def all_todo_keys(self): return self._todos + self._dones @property - def filename(self): + def filename(self) -> str: """ Return a path to the source file or similar information. If the org objects are not loaded from a file, this value will be a string of the form ````. - - :rtype: str - """ return self._filename @@ -473,25 +495,18 @@ class OrgBaseNode(Sequence): 5 """ - _body_lines: List[str] # set by the child classes - - def __init__(self, env, index=None) -> None: - """ - Create an :class:`OrgBaseNode` object. - - :type env: :class:`OrgEnv` - :arg env: This will be set to the :attr:`env` attribute. + _body_lines: list[str] # set by the child classes - """ + def __init__(self, env: OrgEnv, index: int | None = None) -> None: self.env = env self.linenumber = cast(int, None) # set in parse_lines # content - self._lines: List[str] = [] + self._lines: list[str] = [] - self._properties: Dict[str, PropertyValue] = {} - self._timestamps: List[OrgDate] = [] + self._properties: dict[str, PropertyValue] = {} + self._timestamps: list[OrgDate] = [] # FIXME: use `index` argument to set index. (Currently it is # done externally in `parse_lines`.) @@ -518,16 +533,14 @@ def __iter__(self): else: break - def __len__(self): + def __len__(self) -> int: return sum(1 for _ in self) - def __nonzero__(self): + def __bool__(self) -> bool: # As self.__len__ returns non-zero value always this is not # needed. This function is only for performance. return True - __bool__ = __nonzero__ # PY3 - def __getitem__(self, key): if isinstance(key, slice): return itertools.islice(self, key.start, key.stop, key.step) @@ -537,22 +550,23 @@ def __getitem__(self, key): for (i, node) in enumerate(self): if i == key: return node - raise IndexError("Out of range {0}".format(key)) + raise IndexError(f"Out of range {key}") else: - raise TypeError("Inappropriate type {0} for {1}" - .format(type(key), type(self))) + raise TypeError(f"Inappropriate type {type(key)} for {type(self)}" + ) # tree structure - def _find_same_level(self, iterable): + def _find_same_level(self, iterable) -> OrgBaseNode | None: for node in iterable: if node.level < self.level: - return + return None if node.level == self.level: return node + return None @property - def previous_same_level(self): + def previous_same_level(self) -> OrgBaseNode | None: """ Return previous node if exists or None otherwise. @@ -574,7 +588,7 @@ def previous_same_level(self): return self._find_same_level(reversed(self.env._nodes[:self._index])) @property - def next_same_level(self): + def next_same_level(self) -> OrgBaseNode | None: """ Return next node if exists or None otherwise. @@ -600,8 +614,9 @@ def _find_parent(self): for node in reversed(self.env._nodes[:self._index]): if node.level < self.level: return node + return None - def get_parent(self, max_level=None): + def get_parent(self, max_level: int | None = None): """ Return a parent node. @@ -751,7 +766,7 @@ def root(self): root = parent @property - def properties(self) -> Dict[str, PropertyValue]: + def properties(self) -> dict[str, PropertyValue]: """ Node properties as a dictionary. @@ -791,7 +806,7 @@ def from_chunk(cls, env, lines): return self def _parse_comments(self): - special_comments: Dict[str, List[str]] = {} + special_comments: dict[str, list[str]] = {} for line in self._lines: parsed = parse_comment(line) if parsed: @@ -825,29 +840,23 @@ def _iparse_properties(self, ilines: Iterator[str]) -> Iterator[str]: # misc @property - def level(self): + def level(self) -> int: """ Level of this node. - - :rtype: int - """ raise NotImplementedError - def _get_tags(self, inher=False) -> Set[str]: + def _get_tags(self, *, inher: bool = False) -> set[str]: # noqa: ARG002 """ Return tags - :arg bool inher: + :arg inher: Mix with tags of all ancestor nodes if ``True``. - - :rtype: set - """ return set() @property - def tags(self) -> Set[str]: + def tags(self) -> set[str]: """ Tags of this and parent's node. @@ -863,7 +872,7 @@ def tags(self) -> Set[str]: return self._get_tags(inher=True) @property - def shallow_tags(self) -> Set[str]: + def shallow_tags(self) -> set[str]: """ Tags defined for this node (don't look-up parent nodes). @@ -879,7 +888,7 @@ def shallow_tags(self) -> Set[str]: return self._get_tags(inher=False) @staticmethod - def _get_text(text, format='plain'): + def _get_text(text, format: str = 'plain'): # noqa: A002 if format == 'plain': return to_plain_text(text) elif format == 'raw': @@ -887,9 +896,9 @@ def _get_text(text, format='plain'): elif format == 'rich': return to_rich_text(text) else: - raise ValueError('format={0} is not supported.'.format(format)) + raise ValueError(f'format={format} is not supported.') - def get_body(self, format='plain') -> str: + def get_body(self, format: str = 'plain') -> str: # noqa: A002 """ Return a string of body text. @@ -928,8 +937,7 @@ def is_root(self): """ return False - def get_timestamps(self, active=False, inactive=False, - range=False, point=False): + def get_timestamps(self, active=False, inactive=False, range=False, point=False): # noqa: FBT002,A002 # will fix later """ Return a list of timestamps in the body text. @@ -1038,14 +1046,14 @@ def __str__(self) -> str: return "\n".join(self._lines) # todo hmm, not sure if it really belongs here and not to OrgRootNode? - def get_file_property_list(self, property): + def get_file_property_list(self, property: str): # noqa: A002 """ Return a list of the selected property """ vals = self._special_comments.get(property.upper(), None) return [] if vals is None else vals - def get_file_property(self, property): + def get_file_property(self, property: str): # noqa: A002 """ Return a single element of the selected property or None if it doesn't exist """ @@ -1055,7 +1063,7 @@ def get_file_property(self, property): elif len(vals) == 1: return vals[0] else: - raise RuntimeError('Multiple values for property {}: {}'.format(property, vals)) + raise RuntimeError(f'Multiple values for property {property}: {vals}') class OrgRootNode(OrgBaseNode): @@ -1071,18 +1079,18 @@ class OrgRootNode(OrgBaseNode): def heading(self) -> str: return '' - def _get_tags(self, inher=False) -> Set[str]: + def _get_tags(self, *, inher: bool = False) -> set[str]: # noqa: ARG002 filetags = self.get_file_property_list('FILETAGS') return set(filetags) @property - def level(self): + def level(self) -> int: return 0 - def get_parent(self, max_level=None): + def get_parent(self, max_level=None): # noqa: ARG002 return None - def is_root(self): + def is_root(self) -> bool: return True # parsers @@ -1111,19 +1119,19 @@ class OrgNode(OrgBaseNode): """ def __init__(self, *args, **kwds) -> None: - super(OrgNode, self).__init__(*args, **kwds) + super().__init__(*args, **kwds) # fixme instead of casts, should organize code in such a way that they aren't necessary self._heading = cast(str, None) - self._level = None - self._tags = cast(List[str], None) + self._level: int | None = None + self._tags = cast(list[str], None) self._todo: Optional[str] = None self._priority = None self._scheduled = OrgDateScheduled(None) self._deadline = OrgDateDeadline(None) self._closed = OrgDateClosed(None) - self._clocklist: List[OrgDateClock] = [] - self._body_lines: List[str] = [] - self._repeated_tasks: List[OrgDateRepeatedTask] = [] + self._clocklist: list[OrgDateClock] = [] + self._body_lines: list[str] = [] + self._repeated_tasks: list[OrgDateRepeatedTask] = [] # parser @@ -1145,10 +1153,11 @@ def _parse_pre(self): def _parse_heading(self) -> None: heading = self._lines[0] - (heading, self._level) = parse_heading_level(heading) + heading_level = parse_heading_level(heading) + if heading_level is not None: + (heading, self._level) = heading_level (heading, self._tags) = parse_heading_tags(heading) - (heading, self._todo) = parse_heading_todos( - heading, self.env.all_todo_keys) + (heading, self._todo) = parse_heading_todos(heading, self.env.all_todo_keys) (heading, self._priority) = parse_heading_priority(heading) self._heading = heading @@ -1218,7 +1227,7 @@ def _iparse_repeated_tasks(self, ilines: Iterator[str]) -> Iterator[str]: \[ (?P [^\]]+) \]''', re.VERBOSE) - def get_heading(self, format='plain'): + def get_heading(self, format: str ='plain') -> str: # noqa: A002 """ Return a string of head text without tags and TODO keywords. @@ -1247,7 +1256,6 @@ def heading(self) -> str: @property def level(self): - return self._level """ Level attribute of this node. Top level node is level 1. @@ -1256,7 +1264,7 @@ def level(self): ... * Node 1 ... ** Node 2 ... ''') - >>> (n1, n2) = root.children + >>> (n1, n2) = list(root[1:]) >>> root.level 0 >>> n1.level @@ -1265,9 +1273,10 @@ def level(self): 2 """ + return self._level @property - def priority(self): + def priority(self) -> str | None: """ Priority attribute of this node. It is None if undefined. @@ -1284,7 +1293,7 @@ def priority(self): """ return self._priority - def _get_tags(self, inher=False) -> Set[str]: + def _get_tags(self, *, inher: bool = False) -> set[str]: tags = set(self._tags) if inher: parent = self.get_parent() diff --git a/src/orgparse/tests/data/00_simple.py b/src/orgparse/tests/data/00_simple.py index c0b23d1..d2de087 100644 --- a/src/orgparse/tests/data/00_simple.py +++ b/src/orgparse/tests/data/00_simple.py @@ -1,17 +1,21 @@ -from typing import Any, Dict, Set +from typing import Any -def nodedict(i, level, todo=None, shallow_tags=set([]), tags=set([])) -> Dict[str, Any]: - return dict( - heading="Heading {0}".format(i), - level=level, - todo=todo, - shallow_tags=shallow_tags, - tags=tags, - ) +def nodedict(i, level, todo=None, shallow_tags=None, tags=None) -> dict[str, Any]: + if tags is None: + tags = set() + if shallow_tags is None: + shallow_tags = set() + return { + "heading": f"Heading {i}", + "level": level, + "todo": todo, + "shallow_tags": shallow_tags, + "tags": tags, + } -def tags(nums) -> Set[str]: +def tags(nums) -> set[str]: return set(map('TAG{0}'.format, nums)) diff --git a/src/orgparse/tests/data/01_attributes.py b/src/orgparse/tests/data/01_attributes.py index d4555de..df720fc 100644 --- a/src/orgparse/tests/data/01_attributes.py +++ b/src/orgparse/tests/data/01_attributes.py @@ -1,73 +1,76 @@ -from typing import Dict, Any +from typing import Any from orgparse.date import ( - OrgDate, OrgDateScheduled, OrgDateDeadline, OrgDateClosed, + OrgDate, OrgDateClock, + OrgDateClosed, + OrgDateDeadline, + OrgDateScheduled, ) -Raw = Dict[str, Any] +Raw = dict[str, Any] -node1: Raw = dict( - heading="A node with a lot of attributes", - priority='A', - scheduled=OrgDateScheduled((2010, 8, 6)), - deadline=OrgDateDeadline((2010, 8, 10)), - closed=OrgDateClosed((2010, 8, 8, 18, 0)), - clock=[ +node1: Raw = { + "heading": "A node with a lot of attributes", + "priority": 'A', + "scheduled": OrgDateScheduled((2010, 8, 6)), + "deadline": OrgDateDeadline((2010, 8, 10)), + "closed": OrgDateClosed((2010, 8, 8, 18, 0)), + "clock": [ OrgDateClock((2010, 8, 8, 17, 40), (2010, 8, 8, 17, 50), 10), OrgDateClock((2010, 8, 8, 17, 00), (2010, 8, 8, 17, 30), 30), ], - properties=dict(Effort=70), - datelist=[OrgDate((2010, 8, 16))], - rangelist=[ + "properties": {"Effort": 70}, + "datelist": [OrgDate((2010, 8, 16))], + "rangelist": [ OrgDate((2010, 8, 7), (2010, 8, 8)), OrgDate((2010, 8, 9, 0, 30), (2010, 8, 10, 13, 20)), OrgDate((2019, 8, 10, 16, 30, 0), (2019, 8, 10, 17, 30, 0)), ], - body="""\ + "body": """\ - <2010-08-16 Mon> DateList - <2010-08-07 Sat>--<2010-08-08 Sun> - <2010-08-09 Mon 00:30>--<2010-08-10 Tue 13:20> RangeList - <2019-08-10 Sat 16:30-17:30> TimeRange""" -) +} -node2: Raw = dict( - heading="A node without any attributed", - priority=None, - scheduled=OrgDateScheduled(None), - deadline=OrgDateDeadline(None), - closed=OrgDateClosed(None), - clock=[], - properties={}, - datelist=[], - rangelist=[], - body="", -) +node2: Raw = { + "heading": "A node without any attributed", + "priority": None, + "scheduled": OrgDateScheduled(None), + "deadline": OrgDateDeadline(None), + "closed": OrgDateClosed(None), + "clock": [], + "properties": {}, + "datelist": [], + "rangelist": [], + "body": "", +} -node3: Raw = dict( - heading="range in deadline", - priority=None, - scheduled=OrgDateScheduled(None), - deadline=OrgDateDeadline((2019, 9, 6, 10, 0), (2019, 9, 6, 11, 20)), - closed=OrgDateClosed(None), - clock=[], - properties={}, - datelist=[], - rangelist=[], - body=" body", -) +node3: Raw = { + "heading": "range in deadline", + "priority": None, + "scheduled": OrgDateScheduled(None), + "deadline": OrgDateDeadline((2019, 9, 6, 10, 0), (2019, 9, 6, 11, 20)), + "closed": OrgDateClosed(None), + "clock": [], + "properties": {}, + "datelist": [], + "rangelist": [], + "body": " body", +} -node4: Raw = dict( - heading="node with a second line but no date", - priority=None, - scheduled=OrgDateScheduled(None), - deadline=OrgDateDeadline(None), - closed=OrgDateClosed(None), - clock=[], - properties={}, - datelist=[], - rangelist=[], - body="body", -) +node4: Raw = { + "heading": "node with a second line but no date", + "priority": None, + "scheduled": OrgDateScheduled(None), + "deadline": OrgDateDeadline(None), + "closed": OrgDateClosed(None), + "clock": [], + "properties": {}, + "datelist": [], + "rangelist": [], + "body": "body", +} data = [node1, node2, node1, node3, node4] diff --git a/src/orgparse/tests/data/02_tree_struct.py b/src/orgparse/tests/data/02_tree_struct.py index 80a8e77..a4ef46c 100644 --- a/src/orgparse/tests/data/02_tree_struct.py +++ b/src/orgparse/tests/data/02_tree_struct.py @@ -1,11 +1,13 @@ -from typing import Any, Dict +from typing import Any -def nodedict(parent, children=[], previous=None, next=None) -> Dict[str, Any]: - return dict(parent_heading=parent, - children_heading=children, - previous_same_level_heading=previous, - next_same_level_heading=next) +def nodedict(parent, children=None, previous=None, next_=None) -> dict[str, Any]: + if children is None: + children = [] + return {'parent_heading': parent, + 'children_heading': children, + 'previous_same_level_heading': previous, + 'next_same_level_heading': next_} data = [nodedict(*args) for args in [ diff --git a/src/orgparse/tests/data/03_repeated_tasks.py b/src/orgparse/tests/data/03_repeated_tasks.py index 18cfe12..fadd5ed 100644 --- a/src/orgparse/tests/data/03_repeated_tasks.py +++ b/src/orgparse/tests/data/03_repeated_tasks.py @@ -1,13 +1,12 @@ -from orgparse.date import OrgDateRepeatedTask, OrgDateDeadline +from orgparse.date import OrgDateDeadline, OrgDateRepeatedTask - -data = [dict( - heading='Pay the rent', - todo='TODO', - deadline=OrgDateDeadline((2005, 10, 1)), - repeated_tasks=[ +data = [{ + 'heading': 'Pay the rent', + 'todo': 'TODO', + 'deadline': OrgDateDeadline((2005, 10, 1)), + 'repeated_tasks': [ OrgDateRepeatedTask((2005, 9, 1, 16, 10, 0), 'TODO', 'DONE'), OrgDateRepeatedTask((2005, 8, 1, 19, 44, 0), 'TODO', 'DONE'), OrgDateRepeatedTask((2005, 7, 1, 17, 27, 0), 'TODO', 'DONE'), ] -)] +}] diff --git a/src/orgparse/tests/data/04_logbook.py b/src/orgparse/tests/data/04_logbook.py index 457c5fa..085b534 100644 --- a/src/orgparse/tests/data/04_logbook.py +++ b/src/orgparse/tests/data/04_logbook.py @@ -1,11 +1,11 @@ from orgparse.date import OrgDateClock -data = [dict( - heading='LOGBOOK drawer test', - clock=[ +data = [{ + 'heading': 'LOGBOOK drawer test', + 'clock': [ OrgDateClock((2012, 10, 26, 16, 1)), OrgDateClock((2012, 10, 26, 14, 50), (2012, 10, 26, 15, 00)), OrgDateClock((2012, 10, 26, 14, 30), (2012, 10, 26, 14, 40)), OrgDateClock((2012, 10, 26, 14, 10), (2012, 10, 26, 14, 20)), ] -)] +}] diff --git a/src/orgparse/tests/data/05_tags.py b/src/orgparse/tests/data/05_tags.py index 52aee63..f4038e8 100644 --- a/src/orgparse/tests/data/05_tags.py +++ b/src/orgparse/tests/data/05_tags.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- def nodedict(i, tags): - return dict( - heading="Node {0}".format(i), - tags=set(tags), - ) + return { + "heading": f"Node {i}", + "tags": set(tags), + } data = [ @@ -17,7 +16,7 @@ def nodedict(i, tags): [["@_"]], [["_tag_"]], ])] + [ - dict(heading='Heading: :with:colon:', tags=set(["tag"])), + {"heading": 'Heading: :with:colon:', "tags": {"tag"}}, ] + [ - dict(heading='unicode', tags=set(['ёж', 'tag', 'háček'])), + {"heading": 'unicode', "tags": {'ёж', 'tag', 'háček'}}, ] diff --git a/src/orgparse/tests/test_data.py b/src/orgparse/tests/test_data.py index 60e4db0..642ee53 100644 --- a/src/orgparse/tests/test_data.py +++ b/src/orgparse/tests/test_data.py @@ -1,12 +1,12 @@ -from glob import glob import os -from pathlib import Path import pickle - -from .. import load, loads +from glob import glob +from pathlib import Path import pytest +from .. import load, loads + DATADIR = os.path.join(os.path.dirname(__file__), 'data') @@ -34,13 +34,13 @@ def value_from_data_key(node, key): if othernode and not othernode.is_root(): return othernode.heading else: - return + return None else: return getattr(node, key) def data_path(dataname, ext): - return os.path.join(DATADIR, '{0}.{1}'.format(dataname, ext)) + return os.path.join(DATADIR, f'{dataname}.{ext}') def get_datanames(): @@ -60,8 +60,8 @@ def test_data(dataname): for (i, (node, kwds)) in enumerate(zip(root[1:], data)): for key in kwds: val = value_from_data_key(node, key) - assert kwds[key] == val, 'check value of {0}-th node of key "{1}" from "{2}".\n\nParsed:\n{3}\n\nReal:\n{4}'.format(i, key, dataname, val, kwds[key]) - assert type(kwds[key]) == type(val), 'check type of {0}-th node of key "{1}" from "{2}".\n\nParsed:\n{3}\n\nReal:\n{4}'.format(i, key, dataname, type(val), type(kwds[key])) + assert kwds[key] == val, f'check value of {i}-th node of key "{key}" from "{dataname}".\n\nParsed:\n{val}\n\nReal:\n{kwds[key]}' + assert type(kwds[key]) == type(val), f'check type of {i}-th node of key "{key}" from "{dataname}".\n\nParsed:\n{type(val)}\n\nReal:\n{type(kwds[key])}' # noqa: E721 assert root.env.filename == oname diff --git a/src/orgparse/tests/test_date.py b/src/orgparse/tests/test_date.py index 0f39575..39de638 100644 --- a/src/orgparse/tests/test_date.py +++ b/src/orgparse/tests/test_date.py @@ -1,6 +1,13 @@ -from orgparse.date import OrgDate, OrgDateScheduled, OrgDateDeadline, OrgDateClock, OrgDateClosed import datetime +from orgparse.date import ( + OrgDate, + OrgDateClock, + OrgDateClosed, + OrgDateDeadline, + OrgDateScheduled, +) + def test_date_as_string() -> None: @@ -39,4 +46,4 @@ def test_date_as_datetime() -> None: testdatetime = (2021, 9, 3, 16, 19, 13) assert OrgDate._as_datetime(datetime.date(*testdate)) == datetime.datetime(*testdate, 0, 0, 0) - assert OrgDate._as_datetime(datetime.datetime(*testdatetime)) == datetime.datetime(*testdatetime) \ No newline at end of file + assert OrgDate._as_datetime(datetime.datetime(*testdatetime)) == datetime.datetime(*testdatetime) diff --git a/src/orgparse/tests/test_hugedata.py b/src/orgparse/tests/test_hugedata.py index f7248ca..aaa7933 100644 --- a/src/orgparse/tests/test_hugedata.py +++ b/src/orgparse/tests/test_hugedata.py @@ -7,10 +7,9 @@ def generate_org_lines(num_top_nodes, depth=3, nodes_per_level=1, _level=1): if depth == 0: return for i in range(num_top_nodes): - yield ("*" * _level) + ' {0}-th heading of level {1}'.format(i, _level) - for child in generate_org_lines( - nodes_per_level, depth - 1, nodes_per_level, _level + 1): - yield child + yield ("*" * _level) + f' {i}-th heading of level {_level}' + yield from generate_org_lines( + nodes_per_level, depth - 1, nodes_per_level, _level + 1) def num_generate_org_lines(num_top_nodes, depth=3, nodes_per_level=1): diff --git a/src/orgparse/tests/test_misc.py b/src/orgparse/tests/test_misc.py index 4cd73e4..5c0b3ff 100644 --- a/src/orgparse/tests/test_misc.py +++ b/src/orgparse/tests/test_misc.py @@ -1,6 +1,7 @@ +from orgparse.date import OrgDate + from .. import load, loads from ..node import OrgEnv -from orgparse.date import OrgDate def test_empty_heading() -> None: @@ -56,7 +57,7 @@ def test_stars(): ** Subheading with text (B1) This subheading is a child of the "anonymous" heading (B), not of heading (A). - """) + """) # noqa: W291 [h1, h2] = root.children assert h1.heading == 'Heading with text (A)' assert h2.heading == '' @@ -95,7 +96,7 @@ def test_add_custom_todo_keys(): todo_keys = ['CUSTOM_TODO'] done_keys = ['CUSTOM_DONE'] filename = '' # default for loads - content = """#+TODO: COMMENT_TODO | COMMENT_DONE + content = """#+TODO: COMMENT_TODO | COMMENT_DONE """ env = OrgEnv(filename=filename) @@ -236,10 +237,10 @@ def test_date_with_cookies() -> None: ('<2007-05-16 Wed 12:30 +1w>', "OrgDate((2007, 5, 16, 12, 30, 0), None, True, ('+', 1, 'w'))"), ] - for (input, expected) in testcases: - root = loads(input) + for (inp, expected) in testcases: + root = loads(inp) output = root[0].datelist[0] - assert str(output) == input + assert str(output) == inp assert repr(output) == expected testcases = [ ('<2006-11-02 Thu 20:00-22:00 +1w>', @@ -247,8 +248,8 @@ def test_date_with_cookies() -> None: ('<2006-11-02 Thu 20:00--22:00 +1w>', "OrgDate((2006, 11, 2, 20, 0, 0), (2006, 11, 2, 22, 0, 0), True, ('+', 1, 'w'))"), ] - for (input, expected) in testcases: - root = loads(input) + for (inp, expected) in testcases: + root = loads(inp) output = root[0].rangelist[0] assert str(output) == "<2006-11-02 Thu 20:00--22:00 +1w>" assert repr(output) == expected @@ -270,8 +271,8 @@ def test_date_with_cookies() -> None: "<2005-10-01 Sat .+1m>", "OrgDateDeadline((2005, 10, 1), None, True, ('.+', 1, 'm'))"), ] - for (input, expected_str, expected_repr) in testcases2: - root = loads(input) + for (inp, expected_str, expected_repr) in testcases2: + root = loads(inp) output = root[1].deadline assert str(output) == expected_str assert repr(output) == expected_repr @@ -292,8 +293,8 @@ def test_date_with_cookies() -> None: "<2005-10-01 Sat .+1m>", "OrgDateScheduled((2005, 10, 1), None, True, ('.+', 1, 'm'))"), ] - for (input, expected_str, expected_repr) in testcases2: - root = loads(input) + for (inp, expected_str, expected_repr) in testcases2: + root = loads(inp) output = root[1].scheduled assert str(output) == expected_str assert repr(output) == expected_repr diff --git a/src/orgparse/tests/test_rich.py b/src/orgparse/tests/test_rich.py index 7fb911b..e423b0d 100644 --- a/src/orgparse/tests/test_rich.py +++ b/src/orgparse/tests/test_rich.py @@ -1,11 +1,11 @@ ''' Tests for rich formatting: tables etc. ''' -from .. import load, loads -from ..extra import Table - import pytest +from .. import loads +from ..extra import Table + def test_table() -> None: root = loads(''' diff --git a/tox.ini b/tox.ini index 681618b..c99ef94 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,7 @@ [tox] minversion = 3.21 # relies on the correct version of Python installed -envlist = tests,mypy -# FIXME will fix ruff in a later commit +envlist = ruff,tests,mypy # https://github.com/tox-dev/tox/issues/20#issuecomment-247788333 # hack to prevent .tox from crapping to the project directory toxworkdir = {env:TOXWORKDIR_BASE:}{toxinidir}/.tox