Skip to content

Commit 6d3706a

Browse files
[FEATURE] Improved implementation of new cioutput tool (- WIP #265 -)
Changes in file .github/tools/cioutput.py: * added `--title` support * added `--group` support * added `--end-line` and `--end-col` support * related work Changes in file Makefile: * related work Changes in file pytest.ini: * possible workaround for coverage regression Changes in file tests/__init__.py: * improved test loading slightly Changes in file tests/check_pip: * refactored to use new cioutput tool * related work Changes in file tests/check_spelling: * refactored to use new cioutput tool * related work
1 parent ad12ffe commit 6d3706a

File tree

6 files changed

+168
-68
lines changed

6 files changed

+168
-68
lines changed

.github/tools/cioutput.py

Lines changed: 115 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
import logging
6868
import argparse
6969
from enum import Enum, auto
70-
from typing import Optional, Dict, Any, List, Union, TextIO
70+
from typing import Optional
7171

7272
# Module initialization
7373
__module__ = os.path.basename(__file__)
@@ -105,7 +105,7 @@ class GithubActionsFormatter(logging.Formatter):
105105
logging.ERROR: "error",
106106
logging.CRITICAL: "error"
107107
}
108-
108+
109109
def format(self, record: logging.LogRecord) -> str:
110110
"""Format the log record as a GitHub Actions annotation.
111111
@@ -116,14 +116,23 @@ def format(self, record: logging.LogRecord) -> str:
116116
Formatted string in GitHub Actions annotation format
117117
"""
118118
# Extract file/line info if available in extra attributes
119+
is_boundry = getattr(record, "is_boundry", False)
119120
title = getattr(record, "title", None)
120121
file_path = getattr(record, "file_path", None)
121122
line_num = getattr(record, "line_num", None)
123+
end_line_num = getattr(record, "end_line_num", None)
122124
col_num = getattr(record, "col_num", None)
125+
end_col_num = getattr(record, "end_col_num", None)
123126
# Get the annotation level based on log level
124127
annotation_level = self.LEVEL_MAPPING.get(record.levelno, "notice")
125128
# Format the basic message
126129
message = super().format(record)
130+
if is_boundry:
131+
if message and (len(message) > 0):
132+
annotation_level = "group"
133+
return f"::group::{message}"
134+
else:
135+
return "::endgroup::"
127136
# Create the annotation command
128137
command = f"::{annotation_level}"
129138
if annotation_level not in "debug":
@@ -132,8 +141,12 @@ def format(self, record: logging.LogRecord) -> str:
132141
command += f" file={file_path}"
133142
if line_num:
134143
command += f",line={line_num}"
144+
if end_line_num:
145+
command += f",endLine={end_line_num}"
135146
if col_num:
136147
command += f",col={col_num}"
148+
if end_col_num:
149+
command += f",endCol={end_col_num}"
137150
if title:
138151
if file_path:
139152
command += f",title={title}"
@@ -152,11 +165,11 @@ class MarkdownFormatter(logging.Formatter):
152165

153166
# Map between log levels and markdown formatting
154167
LEVEL_MAPPING = {
155-
logging.DEBUG: ("💬", "Debug"),
156-
logging.INFO: ("ℹ️", "Info"),
157-
logging.WARNING: ("⚠️", "Warning"),
158-
logging.ERROR: ("", "Error"),
159-
logging.CRITICAL: ("🔥", "Critical")
168+
logging.DEBUG: (":speech_balloon:", "NOTE"),
169+
logging.INFO: (":information_source:", "NOTE"),
170+
logging.WARNING: (":warning:", "WARNING"),
171+
logging.ERROR: (":x:", "CAUTION"),
172+
logging.CRITICAL: (":fire:", "CAUTION")
160173
}
161174

162175
def format(self, record: logging.LogRecord) -> str:
@@ -170,20 +183,33 @@ def format(self, record: logging.LogRecord) -> str:
170183
"""
171184
# Get the icon and level name
172185
icon, level_name = self.LEVEL_MAPPING.get(
173-
record.levelno, ("ℹ️", "Info")
186+
record.levelno, (":information_source:", "Info")
174187
)
188+
is_boundry = getattr(record, "is_boundry", False)
175189
# Format the basic message
176190
message = super().format(record)
191+
if is_boundry:
192+
if message and (len(message) > 0):
193+
return f"<details><summary>{message}</summary>\n"
194+
else:
195+
return "</details>"
177196
# Extract file/line info if available in extra attributes
197+
title_info = ""
198+
title = getattr(record, "title", None)
178199
file_info = ""
179200
file_path = getattr(record, "file_path", None)
180201
line_num = getattr(record, "line_num", None)
202+
end_line_num = getattr(record, "end_line_num", None)
203+
if title:
204+
title_info = f"**{title}**{icon}\n> "
181205
if file_path:
182-
file_info = f"\n> 📁 `{file_path}`"
206+
file_info = f"\n> :file_folder: `{file_path}`"
183207
if line_num:
184208
file_info += f":{line_num}"
209+
if end_line_num:
210+
file_info += f"-{end_line_num}"
185211
# Format as markdown
186-
return f"{icon} **{level_name}**: {message}{file_info}"
212+
return f"> [!{level_name}]\n> {title_info}{message}{file_info}"
187213

188214

189215
class ColorizedConsoleFormatter(logging.Formatter):
@@ -214,8 +240,14 @@ def format(self, record: logging.LogRecord) -> str:
214240
"""
215241
# Check if output is a terminal before using colors
216242
use_colors = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
243+
is_boundry = getattr(record, "is_boundry", False)
217244
# Format the basic message
218245
message = super().format(record)
246+
if is_boundry:
247+
if len(message) > 0:
248+
return f"[START] {message}"
249+
else:
250+
return "[ END ]"
219251
if use_colors:
220252
# Get the color code for this level
221253
color = self.COLORS.get(record.levelno, self.RESET)
@@ -313,29 +345,42 @@ def log(
313345
self,
314346
message: str,
315347
level: LogLevel = LogLevel.INFO,
348+
is_boundry: Optional[bool] = None,
316349
title: Optional[str] = None,
317350
file_path: Optional[str] = None,
318351
line_num: Optional[int] = None,
319-
col_num: Optional[int] = None
352+
end_line_num: Optional[int] = None,
353+
col_num: Optional[int] = None,
354+
end_col_num: Optional[int] = None
320355
) -> None:
321356
"""Log a message with the configured formatter.
322357
323358
Args:
324359
message: Message to log
325360
level: Log level to use
361+
group: Optional title for annotations
362+
title: Optional title for annotations
326363
file_path: Optional file path for annotations
327364
line_num: Optional line number for annotations
365+
end_line_num: Optional end-line number for annotations
328366
col_num: Optional column number for annotations
367+
end_col_num: Optional end-column number for annotations
329368
"""
330369
extra = {}
370+
if is_boundry:
371+
extra["is_boundry"] = is_boundry
331372
if file_path:
332373
extra["title"] = title
333374
if file_path:
334375
extra["file_path"] = file_path
335376
if line_num:
336377
extra["line_num"] = line_num
378+
if end_line_num:
379+
extra["end_line_num"] = end_line_num
337380
if col_num:
338381
extra["col_num"] = col_num
382+
if end_col_num:
383+
extra["end_col_num"] = end_col_num
339384
self.logger.log(level.value, message, extra=extra if extra else None)
340385
# Also add ERROR and higher messages to GitHub Actions summary
341386
if level.value >= logging.ERROR and self.format_type == OutputFormat.MARKDOWN:
@@ -362,7 +407,7 @@ def debug(self, message: str, **kwargs) -> None:
362407
**kwargs: Additional parameters (title, file_path, line_num, col_num)
363408
"""
364409
self.log(message, LogLevel.DEBUG, **kwargs)
365-
410+
366411
def info(self, message: str, **kwargs) -> None:
367412
"""Log an info message.
368413
@@ -371,7 +416,7 @@ def info(self, message: str, **kwargs) -> None:
371416
**kwargs: Additional parameters (title, file_path, line_num, col_num)
372417
"""
373418
self.log(message, LogLevel.INFO, **kwargs)
374-
419+
375420
def warning(self, message: str, **kwargs) -> None:
376421
"""Log a warning message.
377422
@@ -380,7 +425,7 @@ def warning(self, message: str, **kwargs) -> None:
380425
**kwargs: Additional parameters (title, file_path, line_num, col_num)
381426
"""
382427
self.log(message, LogLevel.WARNING, **kwargs)
383-
428+
384429
def error(self, message: str, **kwargs) -> None:
385430
"""Log an error message.
386431
@@ -389,7 +434,7 @@ def error(self, message: str, **kwargs) -> None:
389434
**kwargs: Additional parameters (title, file_path, line_num, col_num)
390435
"""
391436
self.log(message, LogLevel.ERROR, **kwargs)
392-
437+
393438
def critical(self, message: str, **kwargs) -> None:
394439
"""Log a critical message.
395440
@@ -416,7 +461,7 @@ def configure_output_tool() -> CIOutputTool:
416461
Configured CIOutputTool instance
417462
"""
418463
parser = argparse.ArgumentParser(
419-
description="CI/CD output formatting tool for GitHub Actions"
464+
description="CI/CD output formatting tool for GitHub Actions",
420465
)
421466
parser.add_argument(
422467
"-f", "--format",
@@ -436,29 +481,72 @@ def configure_output_tool() -> CIOutputTool:
436481
help="Optional message to log"
437482
)
438483
parser.add_argument(
439-
"--title",
484+
"-t", "--title",
440485
help="File path for GitHub Actions annotations"
441486
)
442487
parser.add_argument(
443488
"--file",
444489
help="File path for GitHub Actions annotations"
445490
)
446-
parser.add_argument(
491+
group = parser.add_argument_group()
492+
lineGroup = group.add_argument_group()
493+
lineSubGroup = lineGroup.add_mutually_exclusive_group(required=False)
494+
colGroup = lineGroup.add_argument_group()
495+
colSubGroup = colGroup.add_mutually_exclusive_group(required=False)
496+
lineSubGroup.add_argument(
447497
"--line",
498+
dest="line",
448499
type=int,
500+
metavar="LINE",
449501
help="Line number for GitHub Actions annotations"
450502
)
451-
parser.add_argument(
503+
lineSubGroup.add_argument(
504+
"--start-line",
505+
dest="line",
506+
type=int,
507+
metavar="START_LINE",
508+
help="Line number for GitHub Actions annotations"
509+
)
510+
colSubGroup.add_argument(
452511
"--col",
512+
dest="col",
453513
type=int,
514+
metavar="COL",
454515
help="Column number for GitHub Actions annotations"
455516
)
517+
colSubGroup.add_argument(
518+
"--start-col",
519+
dest="col",
520+
type=int,
521+
metavar="START_COL",
522+
help="Column number for GitHub Actions annotations"
523+
)
524+
lineGroup.add_argument(
525+
"--end-line",
526+
dest="end_line",
527+
type=int,
528+
help="End line number for GitHub Actions annotations"
529+
)
530+
colGroup.add_argument(
531+
"--end-col",
532+
dest="end_col",
533+
type=int,
534+
metavar="END_COL",
535+
help="End column number for GitHub Actions annotations"
536+
)
456537
parser.add_argument(
457538
"--log-level",
458539
choices=["debug", "info", "warning", "error", "critical"],
459540
default="info",
460541
help="Minimum log level to display"
461542
)
543+
parser.add_argument(
544+
"--group",
545+
dest="is_group",
546+
default=False,
547+
action="store_true",
548+
help="Optionally treat this as a group boundry. Ends if message is empty, otherwise starts"
549+
)
462550
args = parser.parse_args()
463551
# Determine output format
464552
if args.format == "auto":
@@ -474,15 +562,18 @@ def configure_output_tool() -> CIOutputTool:
474562
# Create and configure the tool
475563
tool = CIOutputTool(format_type, log_level)
476564
# Process message if provided
477-
if args.message:
565+
if args.message or args.is_group:
478566
msg_level = getattr(LogLevel, args.level.upper())
479567
tool.log(
480-
args.message,
568+
args.message if args.message else "",
481569
level=msg_level,
570+
is_boundry=args.is_group,
482571
title=args.title,
483572
file_path=args.file,
484573
line_num=args.line,
574+
end_line_num=args.end_line,
485575
col_num=args.col,
576+
end_col_num=args.end_col,
486577
)
487578
return tool
488579

@@ -495,10 +586,11 @@ def main() -> int:
495586
"""
496587
try:
497588
tool = configure_output_tool()
498-
return 0
589+
if tool:
590+
return 0
499591
except Exception as e:
500592
print(f"Error initializing CI output tool: {e}", file=sys.stderr)
501-
return 1
593+
return 1
502594

503595

504596
if __name__ == "__main__":

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ test-mat-doctests: test-reports MANIFEST.in ## Run doctests MAT category (doctes
280280
$(COVERAGE) run -p --source=multicast -m tests.run_selective --group mat --category doctests || DO_FAIL="exit 2" ; \
281281
$(QUIET)$(WAIT) ; \
282282
$(COVERAGE) combine --keep --data-file=coverage_doctests ./.coverage.* 2>$(ERROR_LOG_PATH) || : ; \
283-
$(COVERAGE) report -m --include=* --data-file=coverage_doctests 2>$(ERROR_LOG_PATH) || : ; \
283+
$(COVERAGE) report -m --include=multicast/* --data-file=coverage_doctests 2>$(ERROR_LOG_PATH) || : ; \
284284
$(COVERAGE) xml -o test-reports/coverage_doctests.xml --include=multicast/* --data-file=coverage_doctests 2>$(ERROR_LOG_PATH) || : ; \
285285
fi
286286
$(QUIET)$(WAIT) ;
@@ -457,7 +457,7 @@ must_be_root:
457457
user-install: build
458458
$(QUIET)$(PYTHON) -m pip install $(PIP_COMMON_FLAGS) $(PIP_ENV_FLAGS) --user "pip>=24.3.1" "setuptools>=75.0" "wheel>=0.44" "build>=1.1.1" 2>$(ERROR_LOG_PATH) || true
459459
$(QUIET)$(PYTHON) -m pip install $(PIP_COMMON_FLAGS) $(PIP_ENV_FLAGS) --user -r "https://raw.githubusercontent.com/reactive-firewall/multicast/stable/requirements.txt" 2>$(ERROR_LOG_PATH) || true
460-
$(QUIET)$(PYTHON) -m pip install $(PIP_COMMON_FLAGS) $(PIP_ENV_FLAGS) --user -e "git+https://github.com/reactive-firewall/multicast.git#egg=multicast"
460+
$(QUIET)$(PYTHON) -m pip install $(PIP_COMMON_FLAGS) $(PIP_ENV_FLAGS) --user dist/multicast-*-py3-*.whl
461461
$(QUIET)$(WAIT)
462462
$(QUIET)$(ECHO) "$@: Done."
463463

pytest.ini

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
[pytest]
22
addopts = --cache-clear --doctest-glob=**/*.py --doctest-modules --cov=multicast --cov-append --cov-report=xml --rootdir=.
3-
testpaths = tests
43
pythonpath = multicast tests
54
python_files = test_*.py
65
python_classes = *TestSuite

tests/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,10 +355,19 @@ def loadDocstringsFromModule(module: types.ModuleType) -> TestSuite:
355355
except Exception: # pragma: no branch
356356
_LOGGER.warning("Error loading optional debug tests", exc_info=True)
357357

358+
try:
359+
EXTRA_TESTS["coverage"].append(loadDocstringsFromModule(__module__))
360+
EXTRA_TESTS["coverage"].append(loadDocstringsFromModule(context))
361+
EXTRA_TESTS["coverage"].append(loadDocstringsFromModule("tests.MulticastUDPClient"))
362+
except Exception: # pragma: no branch
363+
_LOGGER.warning("Error loading optional doctests", exc_info=True)
364+
358365
try:
359366
from tests import test_extra
360367
depends.insert(11, test_extra)
361368
EXTRA_TESTS["security"].append(test_extra.ExtraDocsUtilsTestSuite)
369+
import docs.utils
370+
EXTRA_TESTS["security"].append(loadDocstringsFromModule(docs.utils))
362371
except Exception: # pragma: no branch
363372
_LOGGER.warning("Error loading optional extra tests", exc_info=True)
364373

0 commit comments

Comments
 (0)