aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--setup.cfg3
-rw-r--r--yapymake/__init__.py3
-rw-r--r--yapymake/args.py23
-rw-r--r--yapymake/makefile/__init__.py92
-rw-r--r--yapymake/makefile/parse_util.py64
-rw-r--r--yapymake/makefile/token.py40
-rw-r--r--yapymake/util/__init__.py4
7 files changed, 131 insertions, 98 deletions
diff --git a/setup.cfg b/setup.cfg
index 16c2abd..4e55d91 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -26,3 +26,6 @@ where = .
[options.entry_points]
console_scripts =
yapymake = yapymake:main
+
+[mypy]
+strict = True
diff --git a/yapymake/__init__.py b/yapymake/__init__.py
index fe4b182..8a0c31d 100644
--- a/yapymake/__init__.py
+++ b/yapymake/__init__.py
@@ -4,7 +4,7 @@ DESCRIPTION = 'A (mostly) POSIX-compatible make implemented in Python'
from .args import parse
from .makefile import Makefile
-def main():
+def main() -> None:
these_args = parse()
file = Makefile(these_args)
# TODO dump command line into MAKEFLAGS
@@ -15,6 +15,7 @@ def main():
targets = [arg for arg in these_args.targets_or_macros if '=' not in arg]
if len(targets) == 0:
+ assert file.first_non_special_target is not None
targets = [file.first_non_special_target]
for target in targets:
diff --git a/yapymake/args.py b/yapymake/args.py
index 2252855..3f9d842 100644
--- a/yapymake/args.py
+++ b/yapymake/args.py
@@ -1,9 +1,8 @@
import argparse
from dataclasses import dataclass
-import io
import os
import sys
-from typing import List, TextIO
+from typing import List, Optional, TextIO
from . import DESCRIPTION, VERSION
@@ -95,21 +94,25 @@ class Args:
self.touch = parsed_args.touch
self.targets_or_macros = parsed_args.targets_or_macros
-def parse(cli_args: List[str] = None, env_makeflags: str = None) -> Args:
+def parse(cli_args: Optional[List[str]] = None, env_makeflags: Optional[str] = None) -> Args:
if cli_args is None:
- cli_args = sys.argv[1:]
+ real_cli_args = sys.argv[1:]
+ else:
+ real_cli_args = cli_args
if env_makeflags is None:
- env_makeflags = os.environ.get('MAKEFLAGS', '')
+ real_env_makeflags = os.environ.get('MAKEFLAGS', '')
+ else:
+ real_env_makeflags = env_makeflags
# per POSIX, we accept option letters without a leading -, so to simplify we prepend a - now
# TODO allow macro definitions in MAKEFLAGS
- if len(env_makeflags) > 0 and not env_makeflags.startswith('-'):
- env_makeflags = '-' + env_makeflags
+ if len(real_env_makeflags) > 0 and not real_env_makeflags.startswith('-'):
+ real_env_makeflags = '-' + real_env_makeflags
- if len(env_makeflags) > 0:
- all_args = [env_makeflags, *cli_args]
+ if len(real_env_makeflags) > 0:
+ all_args = [real_env_makeflags, *real_cli_args]
else:
- all_args = cli_args
+ all_args = real_cli_args
return Args(parser.parse_args(all_args))
diff --git a/yapymake/makefile/__init__.py b/yapymake/makefile/__init__.py
index 7a2adfe..70b61d8 100644
--- a/yapymake/makefile/__init__.py
+++ b/yapymake/makefile/__init__.py
@@ -32,7 +32,8 @@ class Makefile:
if args.builtin_rules:
self._inference_rules += BUILTIN_INFERENCE_RULES
- self._macros.update(BUILTIN_MACROS)
+ for k, v in BUILTIN_MACROS.items():
+ self._macros[k] = (MacroSource.Builtin, TokenString.text(v))
for target in BUILTIN_TARGETS:
self._targets[target.name] = target
@@ -47,7 +48,7 @@ class Makefile:
# TODO either discern command line vs MAKEFLAGS or don't pretend we can
self._macros[name] = (MacroSource.CommandLine, TokenString.text(value))
- def read(self, file: TextIO):
+ def read(self, file: TextIO) -> None:
lines_iter: PeekableIterator[str] = PeekableIterator(iter(file))
for line in lines_iter:
# handle escaped newlines (POSIX says these are different in command lines (which we handle later) and
@@ -101,21 +102,23 @@ class Makefile:
if line_type == 'rule':
# > Target entries are specified by a <blank>-separated, non-null list of targets, then a <colon>, then
# > a <blank>-separated, possibly empty list of prerequisites.
- targets, after_colon = line_tokens.split_once(':')
- targets = self.expand_macros(targets).split()
+ colon_split = line_tokens.split_once(':')
+ assert colon_split is not None
+ targets_tokens, after_colon = colon_split
+ targets = self.expand_macros(targets_tokens).split()
# > Text following a <semicolon>, if any, and all following lines that begin with a <tab>, are makefile
# > command lines to be executed to update the target.
semicolon_split = after_colon.split_once(';')
if semicolon_split is None:
prerequisites = self.expand_macros(after_colon).split()
- commands = []
+ command_token_strings = []
else:
- prerequisites, commands = semicolon_split
- prerequisites = self.expand_macros(prerequisites).split()
- commands = [commands]
+ prerequisite_tokens, command_tokens = semicolon_split
+ prerequisites = self.expand_macros(prerequisite_tokens).split()
+ command_token_strings = [command_tokens]
while lines_iter.peek().startswith('\t'):
- commands.append(tokenize(next(lines_iter).lstrip('\t')))
- commands = [CommandLine(c) for c in commands]
+ command_token_strings.append(tokenize(next(lines_iter).lstrip('\t')))
+ commands = [CommandLine(c) for c in command_token_strings]
# we don't know yet if it's a target rule or an inference rule
match = re.fullmatch(r'(?P<s1>(\.[^/.]+)?)(?P<s2>\.[^/.]+)', targets[0])
@@ -139,17 +142,19 @@ class Makefile:
elif line_type == 'macro':
# > The macro named string1 is defined as having the value of string2, where string2 is defined as all
# > characters, if any, after the <equals-sign>...
- name, value = line_tokens.split_once('=')
+ equals_split = line_tokens.split_once('=')
+ assert equals_split is not None
+ name_tokens, value = equals_split
# > up to a comment character ( '#' ) or an unescaped <newline>.
comment_split = value.split_once('#')
if comment_split is not None:
value, _ = comment_split
# > Any <blank> characters immediately before or after the <equals-sign> shall be ignored.
- name.rstrip()
+ name_tokens.rstrip()
value.lstrip()
# > Macros in the string before the <equals-sign> in a macro definition shall be evaluated when the
# > macro assignment is made.
- name = self.expand_macros(name)
+ name = self.expand_macros(name_tokens)
# > Macros defined in the makefile(s) shall override macro definitions that occur before them in the
# > makefile(s) and macro definitions from source 4. If the -e option is not specified, macros defined
# > in the makefile(s) shall override macro definitions from source 3. Macros defined in the makefile(s)
@@ -172,47 +177,50 @@ class Makefile:
internal_macro = len(macro_name) in [1, 2] and macro_name[0] in '@?<*' and \
macro_name[1:] in ['', 'D', 'F']
if internal_macro:
+ assert current_target is not None
if macro_name[0] == '@':
# > The $@ shall evaluate to the full target name of the current target, or the archive filename
# > part of a library archive target. It shall be evaluated for both target and inference rules.
- macro_value = [current_target.name]
+ macro_pieces = [current_target.name]
elif macro_name[0] == '?':
# > The $? macro shall evaluate to the list of prerequisites that are newer than the current
# > target. It shall be evaluated for both target and inference rules.
- macro_value = [p for p in current_target.prerequisites if self.target(p).newer_than(current_target)]
+ macro_pieces = [p for p in current_target.prerequisites if self.target(p).newer_than(current_target)]
elif macro_name[0] == '<':
# > In an inference rule, the $< macro shall evaluate to the filename whose existence allowed
# > the inference rule to be chosen for the target. In the .DEFAULT rule, the $< macro shall
# > evaluate to the current target name.
- macro_value = current_target.prerequisites
+ macro_pieces = current_target.prerequisites
elif macro_name[0] == '*':
# > The $* macro shall evaluate to the current target name with its suffix deleted.
- macro_value = [str(PurePath(current_target.name).with_suffix(''))]
+ macro_pieces = [str(PurePath(current_target.name).with_suffix(''))]
else:
# this shouldn't happen
- macro_value = []
+ macro_pieces = []
if macro_name[1:] == 'D':
- macro_value = [str(PurePath(x).parent) for x in macro_value]
+ macro_pieces = [str(PurePath(x).parent) for x in macro_pieces]
elif macro_name[1:] == 'F':
- macro_value = [str(PurePath(x).name) for x in macro_value]
+ macro_pieces = [str(PurePath(x).name) for x in macro_pieces]
- macro_value = TokenString.text(' '.join(macro_value))
+ macro_tokens = TokenString.text(' '.join(macro_pieces))
else:
- _, macro_value = self._macros[this_token.name]
- macro_value = self.expand_macros(macro_value, current_target)
+ _, macro_tokens = self._macros[this_token.name]
+ macro_value = self.expand_macros(macro_tokens, current_target)
if this_token.replacement is not None:
replaced, replacement = (self.expand_macros(t, current_target) for t in this_token.replacement)
macro_value = re.sub(re.escape(replaced) + r'\b', replacement, macro_value)
return macro_value
+ else:
+ raise TypeError('unexpected token type!')
return ''.join(expand_one(t) for t in text)
def special_target(self, name: str) -> Optional['Target']:
return self._targets.get(name, None)
- def special_target_has_prereq(self, target: str, name: str) -> bool:
- target = self.special_target(target)
+ def special_target_has_prereq(self, target_name: str, name: str) -> bool:
+ target = self.special_target(target_name)
if target is None:
return False
return len(target.prerequisites) == 0 or name in target.prerequisites
@@ -269,6 +277,8 @@ class Target:
path = self._path()
if path.exists():
return path.stat().st_mtime
+ else:
+ return None
def newer_than(self, other: 'Target') -> Optional[bool]:
self_mtime = self.modified_time()
@@ -279,6 +289,8 @@ class Target:
return True
elif other_mtime is None and other.already_updated and other.name in self.prerequisites:
return False
+ else:
+ return None
def is_up_to_date(self, file: Makefile) -> bool:
if self.already_updated:
@@ -287,14 +299,14 @@ class Target:
newer_than_all_dependencies = all(self.newer_than(file.target(other)) for other in self.prerequisites)
return exists and newer_than_all_dependencies
- def update(self, file: Makefile):
+ def update(self, file: Makefile) -> None:
for prerequisite in self.prerequisites:
file.target(prerequisite).update(file)
if not self.is_up_to_date(file):
self.execute_commands(file)
self.already_updated = True
- def execute_commands(self, file: Makefile):
+ def execute_commands(self, file: Makefile) -> None:
for command in self.commands:
command.execute(file, self)
@@ -333,7 +345,7 @@ class CommandLine:
first_token.text = first_token.text[1:]
self.execution_line = TokenString(list((first_token, *tokens_iter)))
- def execute(self, file: Makefile, current_target: 'Target'):
+ def execute(self, file: Makefile, current_target: 'Target') -> None:
# POSIX:
# > If the command prefix contains a <hyphen-minus>, or the -i option is present, or the special target .IGNORE
# > has either the current target as a prerequisite or has no prerequisites, any error found while executing
@@ -415,18 +427,18 @@ BUILTIN_INFERENCE_RULES = [
]),
]
BUILTIN_MACROS = {
- 'MAKE': TokenString.text('make'),
- 'AR': TokenString.text('ar'),
- 'ARFLAGS': TokenString.text('-rv'),
- 'YACC': TokenString.text('yacc'),
- 'YFLAGS': TokenString.text(''),
- 'LEX': TokenString.text('lex'),
- 'LFLAGS': TokenString.text(''),
- 'LDFLAGS': TokenString.text(''),
- 'CC': TokenString.text('c99'),
- 'CFLAGS': TokenString.text('-O 1'),
- 'FC': TokenString.text('fort77'),
- 'FFLAGS': TokenString.text('-O 1'),
+ 'MAKE': 'make',
+ 'AR': 'ar',
+ 'ARFLAGS': '-rv',
+ 'YACC': 'yacc',
+ 'YFLAGS': '',
+ 'LEX': 'lex',
+ 'LFLAGS': '',
+ 'LDFLAGS': '',
+ 'CC': 'c99',
+ 'CFLAGS': '-O 1',
+ 'FC': 'fort77',
+ 'FFLAGS': '-O 1',
}
BUILTIN_TARGETS = [
Target('.SUFFIXES', ['.o', '.c', '.y', '.l', '.a', '.sh', '.f'], []),
diff --git a/yapymake/makefile/parse_util.py b/yapymake/makefile/parse_util.py
index ad1e363..ded7476 100644
--- a/yapymake/makefile/parse_util.py
+++ b/yapymake/makefile/parse_util.py
@@ -43,6 +43,7 @@ def tag(tag_text: str) -> Parser[None]:
def parse(text: str) -> ParseResult[None]:
if text.startswith(tag_text):
return None, text[len(tag_text):]
+ return None
return parse
def take_while1(predicate: Callable[[str], bool]) -> Parser[str]:
@@ -61,13 +62,14 @@ def take_till1(predicate: Callable[[str], bool]) -> Parser[str]:
def any_char(text: str) -> ParseResult[str]:
if len(text) > 0:
return text[0], text[1:]
+ return None
def all_consuming(parser: Parser[T]) -> Parser[T]:
def parse(text: str) -> ParseResult[T]:
- result = parser(text)
- if result is None:
+ parsed_result = parser(text)
+ if parsed_result is None:
return None
- result, extra = result
+ result, extra = parsed_result
if len(extra) > 0:
return None
return result, ''
@@ -75,10 +77,10 @@ def all_consuming(parser: Parser[T]) -> Parser[T]:
def map_parser(parser: Parser[T1], mapper: Callable[[T1], T2]) -> Parser[T2]:
def parse(text: str) -> ParseResult[T2]:
- result = parser(text)
- if result is None:
+ parsed_result = parser(text)
+ if parsed_result is None:
return None
- result, extra = result
+ result, extra = parsed_result
return mapper(result), extra
return parse
@@ -92,10 +94,10 @@ def opt(parser: Parser[T]) -> Parser[Optional[T]]:
def verify(parser: Parser[T], predicate: Callable[[T], bool]) -> Parser[T]:
def parse(text: str) -> ParseResult[T]:
- result = parser(text)
- if result is None:
+ parsed_result = parser(text)
+ if parsed_result is None:
return None
- result, extra = result
+ result, extra = parsed_result
if predicate(result):
return result, extra
return None
@@ -106,13 +108,13 @@ def many1(parser: Parser[T]) -> Parser[List[T]]:
parser_result = parser(text)
if parser_result is None:
return None
- parser_result, extra = parser_result
- result = [parser_result]
+ this_result, extra = parser_result
+ result = [this_result]
parser_result = parser(extra)
while parser_result is not None:
- parser_result, extra = parser_result
- result.append(parser_result)
+ this_result, extra = parser_result
+ result.append(this_result)
parser_result = parser(extra)
return result, extra
return parse
@@ -124,10 +126,10 @@ def delimited(before_parser: Parser[T1], parser: Parser[T], after_parser: Parser
return None
_, extra = before_result
- result = parser(extra)
- if result is None:
+ parsed_result = parser(extra)
+ if parsed_result is None:
return None
- result, extra = result
+ result, extra = parsed_result
after_result = after_parser(extra)
if after_result is None:
@@ -139,15 +141,15 @@ def delimited(before_parser: Parser[T1], parser: Parser[T], after_parser: Parser
def pair(first_parser: Parser[T1], second_parser: Parser[T2]) -> Parser[Tuple[T1, T2]]:
def parse(text: str) -> ParseResult[Tuple[T1, T2]]:
- first_result = first_parser(text)
- if first_result is None:
+ first_parsed_result = first_parser(text)
+ if first_parsed_result is None:
return None
- first_result, extra = first_result
+ first_result, extra = first_parsed_result
- second_result = second_parser(extra)
- if second_result is None:
+ second_parsed_result = second_parser(extra)
+ if second_parsed_result is None:
return None
- second_result, extra = second_result
+ second_result, extra = second_parsed_result
return (first_result, second_result), extra
return parse
@@ -159,30 +161,30 @@ def preceded(before_parser: Parser[T1], parser: Parser[T]) -> Parser[T]:
return None
_, extra = before_result
- result = parser(extra)
- if result is None:
+ parsed_result = parser(extra)
+ if parsed_result is None:
return None
- result, extra = result
+ result, extra = parsed_result
return result, extra
return parse
def separated_pair(first_parser: Parser[T1], between_parser: Parser[T], second_parser: Parser[T2]) -> Parser[Tuple[T1, T2]]:
def parse(text: str) -> ParseResult[Tuple[T1, T2]]:
- first_result = first_parser(text)
- if first_result is None:
+ first_parsed_result = first_parser(text)
+ if first_parsed_result is None:
return None
- first_result, extra = first_result
+ first_result, extra = first_parsed_result
between_result = between_parser(extra)
if between_result is None:
return None
_, extra = between_result
- second_result = second_parser(extra)
- if second_result is None:
+ second_parsed_result = second_parser(extra)
+ if second_parsed_result is None:
return None
- second_result, extra = second_result
+ second_result, extra = second_parsed_result
return (first_result, second_result), extra
return parse
diff --git a/yapymake/makefile/token.py b/yapymake/makefile/token.py
index 948fcac..fd7de5b 100644
--- a/yapymake/makefile/token.py
+++ b/yapymake/makefile/token.py
@@ -1,5 +1,5 @@
from dataclasses import dataclass
-from typing import Iterable, Iterator, List, Optional, Tuple
+from typing import Iterable, Iterator, List, MutableSequence, Optional, Tuple
from .parse_util import *
@@ -12,18 +12,19 @@ __all__ = [
]
class TokenString(Iterable['Token']):
- _tokens: List['Token']
+ _tokens: MutableSequence['Token']
- def __init__(self, my_tokens: List['Token'] = None):
+ def __init__(self, my_tokens: Optional[MutableSequence['Token']] = None):
if my_tokens is None:
- my_tokens = []
- self._tokens = my_tokens
+ self._tokens = []
+ else:
+ self._tokens = my_tokens
@staticmethod
def text(body: str) -> 'TokenString':
return TokenString([TextToken(body)])
- def __eq__(self, other) -> bool:
+ def __eq__(self, other: object) -> bool:
return isinstance(other, TokenString) and self._tokens == other._tokens
def __iter__(self) -> Iterator['Token']:
@@ -33,7 +34,7 @@ class TokenString(Iterable['Token']):
return f'TokenString({repr(self._tokens)})'
def split_once(self, delimiter: str) -> Optional[Tuple['TokenString', 'TokenString']]:
- result0 = []
+ result0: List[Token] = []
self_iter = iter(self._tokens)
for t in self_iter:
if isinstance(t, TextToken) and delimiter in t.text:
@@ -44,13 +45,13 @@ class TokenString(Iterable['Token']):
result0.append(t)
return None
- def lstrip(self):
+ def lstrip(self) -> None:
first_token = self._tokens[0]
if isinstance(first_token, TextToken):
first_token.text = first_token.text.lstrip()
self._tokens[0] = first_token
- def rstrip(self):
+ def rstrip(self) -> None:
last_token = self._tokens[-1]
if isinstance(last_token, TextToken):
last_token.text = last_token.text.rstrip()
@@ -67,13 +68,18 @@ class TextToken(Token):
@dataclass()
class MacroToken(Token):
name: str
- replacement: Optional[Tuple[TokenString, TokenString]] = None
+ replacement: Optional[Tuple[TokenString, TokenString]]
macro_name = take_while1(lambda c: c.isalnum() or c in ['.', '_'])
def macro_expansion_body(end: str) -> Parser[MacroToken]:
- subst = preceded(tag(":"), separated_pair(tokens('='), '=', tokens(end)))
- return map_parser(pair(macro_name, opt(subst)), MacroToken)
+ subst = preceded(tag(":"), separated_pair(tokens('='), tag('='), tokens(end)))
+
+ def make_token(data: Tuple[str, Optional[Tuple[TokenString, TokenString]]]) -> MacroToken:
+ name, replacement = data
+ return MacroToken(name, replacement)
+
+ return map_parser(pair(macro_name, opt(subst)), make_token)
def parens_macro_expansion(text: str) -> ParseResult[MacroToken]:
return delimited(tag('$('), macro_expansion_body(')'), tag(')'))(text)
@@ -85,12 +91,12 @@ def build_tiny_expansion(name_probably: str) -> Token:
if name_probably == '$':
return TextToken('$')
else:
- return MacroToken(name_probably)
+ return MacroToken(name_probably, None)
-def tiny_macro_expansion(text: str) -> ParseResult[MacroToken]:
+def tiny_macro_expansion(text: str) -> ParseResult[Token]:
return map_parser(preceded(tag('$'), verify(any_char, lambda c: c not in ['(', '{'])), build_tiny_expansion)(text)
-def macro_expansion(text: str) -> ParseResult[MacroToken]:
+def macro_expansion(text: str) -> ParseResult[Token]:
return alt(tiny_macro_expansion, parens_macro_expansion, braces_macro_expansion)(text)
just_text = map_parser(take_till1(lambda c: c == '$'), TextToken)
@@ -114,7 +120,9 @@ def full_text_tokens(text: str) -> ParseResult[TokenString]:
return all_consuming(tokens())(text)
def tokenize(text: str) -> TokenString:
- result, _ = full_text_tokens(text)
+ parsed_result = full_text_tokens(text)
+ assert parsed_result is not None
+ result, _ = parsed_result
return result
# TODO handle errors
diff --git a/yapymake/util/__init__.py b/yapymake/util/__init__.py
index b26bbcd..04399fd 100644
--- a/yapymake/util/__init__.py
+++ b/yapymake/util/__init__.py
@@ -1 +1,5 @@
+__all__ = [
+ 'PeekableIterator',
+]
+
from .peekable_iterator import PeekableIterator