Skip to content
Open

4.0.0 #368

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
e62a35a
Update _api.py
bxngyn Jul 11, 2025
038dbb7
Update results.html
bxngyn Jul 11, 2025
24c8d24
undo commit
bxngyn Jul 11, 2025
d504890
Update _api.py
bxngyn Jul 11, 2025
d5aaed5
Update _api.py
bxngyn Jul 11, 2025
2bdd070
Update results.html
bxngyn Jul 11, 2025
236479b
Update _api.py
bxngyn Jul 11, 2025
39dc8d5
Update _api.py
bxngyn Jul 11, 2025
399ee45
update truncation and display
bxngyn Jul 14, 2025
836fdc1
test workflow with rationale
bxngyn Jul 14, 2025
9ec5408
test workflow
bxngyn Jul 14, 2025
ccda3bb
dynamic truncation
bxngyn Jul 14, 2025
2febd90
Added feature where (trailing) invisible chars are highlighted
ivanharvard Jul 14, 2025
2d86fb9
Comment fix
ivanharvard Jul 14, 2025
3b1bfd7
Removed unused import
ivanharvard Jul 14, 2025
f93d1ce
update check50_tests.py
bxngyn Jul 14, 2025
fffa2e5
remove comments
bxngyn Jul 15, 2025
ae3a99c
add motivational msgs
bxngyn Jul 15, 2025
dc563c0
tweaked formatting
rongxin-liu Jul 15, 2025
ce3e030
Merge pull request #355 from cs50/patch-invis-chars
rongxin-liu Jul 15, 2025
af90cfb
Use hex value for color in the renderer
rongxin-liu Jul 15, 2025
cdbdfdb
Merge pull request #356 from cs50/patch-invis-char-html
rongxin-liu Jul 15, 2025
dcfb573
update msg display case
bxngyn Jul 15, 2025
74340ba
update probability
bxngyn Jul 15, 2025
dcd23d6
reduce code redundancy
rongxin-liu Jul 15, 2025
5869559
Merge pull request #357 from cs50/add-msg
rongxin-liu Jul 15, 2025
63bfb17
hash
rongxin-liu Jul 15, 2025
b854654
bump version to 4.0.0-dev in setup.py for now
rongxin-liu Jul 15, 2025
3e1e0b0
custom truncation
bxngyn Jul 15, 2025
f7fd5cf
update custom truncation
bxngyn Jul 15, 2025
41b9f3f
updated docs to reflect raised exceptions during timeout more accurately
ivanharvard Jul 15, 2025
0b4c0e1
Merge pull request #360 from cs50/patch-docs-missing-exception
ivanharvard Jul 15, 2025
dd2e27d
Jul 16, 2025, 5:42 PM
ivanharvard Jul 16, 2025
2c1aaf0
added assertion rewrites
ivanharvard Jul 21, 2025
9280ef0
added conditional inferencing
ivanharvard Jul 22, 2025
5c05c72
comment fix
ivanharvard Jul 22, 2025
d57c89f
Merge branch 'develop' into 4.0.0-dev
rongxin-liu Jul 23, 2025
5aa2958
Merge branch '4.0.0-dev' into py-ast
rongxin-liu Jul 23, 2025
777e7d8
import shutil
rongxin-liu Jul 23, 2025
6096558
Merge branch '4.0.0-dev' into patch-invis-chars
rongxin-liu Jul 23, 2025
72d08b2
simplify type handling in _truncate function
rongxin-liu Jul 23, 2025
1860888
Fixed EOF and TIMEOUT bug
ivanharvard Jul 23, 2025
450a3fe
fixed quotes around EOF and TIMEOUT
ivanharvard Jul 23, 2025
0efa116
added warning about use in interactive mode
ivanharvard Jul 23, 2025
1b639cc
added space
ivanharvard Jul 23, 2025
b4454aa
tweaked formatting
rongxin-liu Jul 23, 2025
3650682
Merge pull request #367 from cs50/warn-repl-mode
rongxin-liu Jul 23, 2025
7d3b284
Use tuple membership test for EOF/TIMEOUT check
rongxin-liu Jul 23, 2025
3b5bd4d
added context
ivanharvard Jul 23, 2025
adb4a10
Merge pull request #366 from cs50/fix/issue-365
rongxin-liu Jul 23, 2025
20e3f7c
Merge branch '4.0.0-dev' into py-ast
rongxin-liu Jul 23, 2025
3979873
Merge branch '4.0.0-dev' into patch-invis-chars
rongxin-liu Jul 23, 2025
88d4cfb
wip function evaluation in help msg
ivanharvard Jul 24, 2025
adf5fce
Merge branch 'py-ast' of github.com:cs50/check50 into py-ast
ivanharvard Jul 24, 2025
3e5b8cc
added memoization of evaluatable objects
ivanharvard Jul 25, 2025
2dd33c7
fixed leftover debug message where context was passed with the error …
ivanharvard Jul 25, 2025
c7ff47d
refactored config into its own file
ivanharvard Jul 31, 2025
3736d09
removed toggle and cleaned up
ivanharvard Jul 31, 2025
12d0052
Remove trailing comma
rongxin-liu Jul 31, 2025
73741a5
Update check50/_api.py
rongxin-liu Jul 31, 2025
d1a9547
added boolean integer support
ivanharvard Jul 31, 2025
4f145bd
Add a blank line
rongxin-liu Jul 31, 2025
6e56ab6
Merge pull request #359 from cs50/patch-invis-chars
bxngyn Jul 31, 2025
6c3a28b
Merge branch '4.0.0-dev' into py-ast
ivanharvard Jul 31, 2025
83eb248
Clean up
rongxin-liu Jul 31, 2025
e2e7b0f
added flag support to enable or disable rewrites
ivanharvard Aug 1, 2025
076d474
added tests for assertion rewrite
ivanharvard Aug 1, 2025
c3adca0
add tests for assertions rewrite functionality
rongxin-liu Aug 1, 2025
212c894
remove unused import of ast
rongxin-liu Aug 1, 2025
bf3e119
Merge pull request #361 from cs50/py-ast
rongxin-liu Aug 1, 2025
4234eae
update deprecated set-out usage in workflow
rongxin-liu Aug 1, 2025
16573bc
fixed bug with globals not being imported and incorrect name replacement
ivanharvard Aug 1, 2025
db7c6ff
fixed bugs in which functions and module names would appear in contex…
ivanharvard Aug 1, 2025
cc54d6c
added more documentation and fixed bugs with duplicate expressions an…
ivanharvard Aug 4, 2025
5d20ff7
added flag support
ivanharvard Aug 4, 2025
36bed3d
fixed bug in which local functions were not included in the output up…
ivanharvard Aug 5, 2025
bb798d7
link locale for internationalization
bxngyn Aug 5, 2025
72b1936
added support for --https and --ssh
ivanharvard Aug 5, 2025
8433c1d
Merge pull request #370 from cs50/feat/rewrite-assert-flag
rongxin-liu Aug 6, 2025
d06e909
Merge branch '4.0.0-dev' into feat/add-auth-method-flag
rongxin-liu Aug 6, 2025
f698cf5
added debug messages
ivanharvard Aug 6, 2025
39da322
improve error handling during authentication failure
rongxin-liu Aug 7, 2025
3c50d81
fix syntax error
rongxin-liu Aug 7, 2025
cf10651
Merge pull request #373 from cs50/feat/add-auth-method-flag
rongxin-liu Aug 7, 2025
2250164
update Mismatch class to serialize expected and actual values correct…
rongxin-liu Aug 7, 2025
c9ed51e
Merge pull request #375 from cs50/fix/issue-374-json-serialization
rongxin-liu Aug 7, 2025
52cc80e
vietnamese support
bxngyn Aug 7, 2025
df0f027
simplified error handling during GitHub authentication
rongxin-liu Aug 7, 2025
bcc5bf4
draft of lang contribution guideline
bxngyn Aug 7, 2025
817e460
Merge pull request #372 from cs50/feat/issue-41-internationalize
rongxin-liu Aug 7, 2025
2253579
added _process_list
ivanharvard Aug 7, 2025
4fc0c42
Merge pull request #378 from cs50/fix/issue-376-lists-as-strs
rongxin-liu Aug 7, 2025
d8610a0
increase default truncate length to improve diff visibility
rongxin-liu Aug 8, 2025
483e432
Merge pull request #381 from cs50/patch/issue-380-improve-truncation
rongxin-liu Aug 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- name: Extract program version
id: program_version
run: |
echo ::set-output name=version::$(check50 --version | cut --delimiter ' ' --fields 2)
echo "version=$(check50 --version | cut --delimiter ' ' --fields 2)" >> $GITHUB_OUTPUT

- name: Create Release
if: ${{ github.ref == 'refs/heads/main' }}
Expand Down
48 changes: 48 additions & 0 deletions CONTRIBUTING_LANG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Check50 Language Translations
Thank you for your interest in making CS50's tooling more accessible for all. Before contributing, please read this in full to avoid any complications.

## Instructions
CS50 uses GitHub to host code, track issues and feature requests, as well as accept pull requests. Please do not email staff members regarding specific issues or features.

In order to add or edit a language for `check50`, please follow these steps.

1. Fork the `check50` repository.
2. Once in the `check50` directory, run `pip install babel` and `pip install -e .`
3. If the language you are looking to add already exists in `check50/locale/`, please skip to step 6.
- See all of the 2 letter language codes [here](https://www.loc.gov/standards/iso639-2/php/code_list.php)
4. Generate the template of strings to translate by running `python3 setup.py extract_messages`. This will create a file in `check50/locale/` called `check50.pot`
5. Run `python setup.py init_catalog -l <LANG>`, where `<LANG>` is the 2 letter language code (see [here](https://www.loc.gov/standards/iso639-2/php/code_list.php)), to create a file called `check50.po` located at `check50/locale/<LANG>/LC_MESSAGES/`. This file is where the translations will be inputted.
6. The original English strings are found at every `msgid` occurence. Translations should be inputted directly under at every `msgstr` occurence.
7. To test your translations, run `python3 setup.py compile_catalog` to compile the `check50.po` file into `check50.mo`.
8. `pip3 install .` to install the new version of `check50` containing these translations.

## Design and Formatting
Please follow the formatting of the `msgstr` English strings. For example, if the `msgid` string is

```
msgid ""
"check50 is not intended for use in interactive mode. Some behavior may "
"not function as expected."
```

The `msgstr` string should replicate the spacing of the English string.

Example:
```
msgstr ""
"check50 không thể sử dụng bằng chế độ tương tác, có thể "
"không hoạt động như mong đợi."
```

Instead of:
```
msgstr ""
"check50 không thể sử dụng bằng chế độ tương tác, có thể không hoạt động như mong đợi."
```


## Translation Error Reports


## References
This document was adapted from the open-source contribution guidelines for [Meta's Draft](https://github.com/facebookarchive/draft-js/blob/main/CONTRIBUTING.md)
13 changes: 12 additions & 1 deletion check50/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ def _setup_translation():
_set_version()
_setup_translation()

# Discourage use of check50 in the interactive mode, due to a naming conflict of
# the `_` variable. check50 uses it for translations, but Python stores the
# result of the last expression in a variable called `_`.
import sys
if hasattr(sys, 'ps1') or sys.flags.interactive:
import warnings
warnings.warn(_("check50 is not intended for use in interactive mode. "
"Some behavior may not function as expected."))

from ._api import (
import_checks,
data, _data,
Expand All @@ -38,7 +47,9 @@ def _setup_translation():

from . import regex
from .runner import check
from .config import config
from pexpect import EOF

__all__ = ["import_checks", "data", "exists", "hash", "include", "regex",
"run", "log", "Failure", "Mismatch", "Missing", "check", "EOF"]
"run", "log", "Failure", "Mismatch", "Missing", "check", "EOF",
"config"]
57 changes: 53 additions & 4 deletions check50/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import logging
import os
import platform
import shutil
import site
from pathlib import Path
import subprocess
Expand All @@ -20,12 +21,14 @@
import requests
import termcolor

from . import _exceptions, internal, renderer, __version__
from . import _exceptions, internal, renderer, assertions, __version__
from .contextmanagers import nullcontext
from .runner import CheckRunner

LOGGER = logging.getLogger("check50")

gettext.install("check50", str(importlib.resources.files("check50").joinpath("locale")))

lib50.set_local_path(os.environ.get("CHECK50_PATH", "~/.local/share/check50"))


Expand Down Expand Up @@ -258,6 +261,20 @@ def process_args(args):
if args.ansi_log and "ansi" not in seen_output:
LOGGER.warning(_("--ansi-log has no effect when ansi is not among the output formats"))

if args.https or args.ssh:
if args.offline:
LOGGER.warning(_("Using either --https and --ssh will have no effect when running offline"))
args.auth_method = None
elif args.https and args.ssh:
LOGGER.warning(_("--https and --ssh have no effect when used together"))
args.auth_method = None
elif args.https:
args.auth_method = "https"
else:
args.auth_method = "ssh"
else:
args.auth_method = None


class LoggerWriter:
def __init__(self, logger, level):
Expand All @@ -273,10 +290,10 @@ def flush(self):


def check_version(package_name=__package__, timeout=1):
"""Check for newer version of the package on PyPI"""
"""Check for newer version of the package on PyPI"""
if not __version__:
return

try:
current = packaging.version.parse(__version__)
latest = max(requests.get(f"https://pypi.org/pypi/{package_name}/json", timeout=timeout).json()["releases"], key=packaging.version.parse)
Expand Down Expand Up @@ -333,6 +350,18 @@ def main():
parser.add_argument("--no-install-dependencies",
action="store_true",
help=_("do not install dependencies (only works with --local)"))
parser.add_argument("--assertion-rewrite",
action="store",
nargs="?",
const="enabled",
choices=["true", "enabled", "1", "on", "false", "disabled", "0", "off"],
help=_("enable or disable assertion rewriting; overrides ENABLE_CHECK50_ASSERT flag in the checks file"))
parser.add_argument("--https",
action="store_true",
help=_("force authentication via HTTPS"))
parser.add_argument("--ssh",
action="store_true",
help=_("force authentication via SSH"))
parser.add_argument("-V", "--version",
action="version",
version=f"%(prog)s {__version__}")
Expand Down Expand Up @@ -387,7 +416,27 @@ def main():
if not args.no_install_dependencies:
install_dependencies(config["dependencies"])

checks_file = (internal.check_dir / config["checks"]).resolve()
# Store the original checks file and leave as is
original_checks_file = (internal.check_dir / config["checks"]).resolve()

# If the user has enabled the rewrite feature
assertion_rewrite_enabled = False
if args.assertion_rewrite is not None:
assertion_rewrite_enabled = args.assertion_rewrite.lower() in ("true", "1", "enabled", "on")
else:
assertion_rewrite_enabled = assertions.rewrite_enabled(str(original_checks_file))

if assertion_rewrite_enabled:
# Create a temporary copy of the checks file
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as tmp:
checks_file = Path(tmp.name)
shutil.copyfile(original_checks_file, checks_file)

# Rewrite all assert statements in the copied checks file to check50_assert
assertions.rewrite(str(checks_file))
else:
# Don't rewrite any assert statements and continue
checks_file = original_checks_file

# Have lib50 decide which files to include
included_files = lib50.files(config.get("files"))[0]
Expand Down
111 changes: 98 additions & 13 deletions check50/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from pexpect.exceptions import EOF, TIMEOUT

from . import internal, regex
from .config import config

_log = []
internal.register.before_every(_log.clear)
Expand Down Expand Up @@ -238,7 +239,9 @@ def stdout(self, output=None, str_output=None, regex=True, timeout=3, show_timeo
:type show_timeout: bool
:raises check50.Mismatch: if ``output`` is specified and nothing that the \
process outputs matches it
:raises check50.Failure: if process times out or if it outputs invalid UTF-8 text.
:raises check50.Missing: if the process times out
:raises check50.Failure: if the process outputs invalid UTF-8 text or \
otherwise fails to verify output

Example usage::

Expand Down Expand Up @@ -422,6 +425,8 @@ class Missing(Failure):
"""

def __init__(self, missing_item, collection, help=None):
if isinstance(collection, list):
collection = _process_list(collection, _raw)
super().__init__(rationale=_("Did not find {} in {}").format(_raw(missing_item), _raw(collection)), help=help)

if missing_item == EOF:
Expand Down Expand Up @@ -453,15 +458,19 @@ class Mismatch(Failure):
"""

def __init__(self, expected, actual, help=None):
super().__init__(rationale=_("expected {}, not {}").format(_raw(expected), _raw(actual)), help=help)
def _safe_truncate(x, y):
return _truncate(x, y) if x not in (EOF, TIMEOUT) else x

if expected == EOF:
expected = "EOF"
expected, actual = _safe_truncate(expected, actual), _safe_truncate(actual, expected)

if actual == EOF:
actual = "EOF"
rationale = _("expected: {}\n actual: {}").format(
_raw(expected),
_raw(actual)
)

self.payload.update({"expected": expected, "actual": actual})
super().__init__(rationale=rationale, help=help)

self.payload.update({"expected": _raw(expected), "actual": _raw(actual)})


def hidden(failure_rationale):
Expand Down Expand Up @@ -493,19 +502,95 @@ def wrapper(*args, **kwargs):
return wrapper
return decorator

def _process_list(lst, processor, flatten="shallow", joined_by="\n"):
"""
Applies a function `processor` to every element of a list.

`flatten` has 3 choices:
- `none`: Apply `processor` to every element of a list without flattening (e.g. `['1', '2', '[3]']`).
- `shallow`: Flatten by one level only and apply `processor` (e.g. `'1\\n2\\n[3]'`).
- `deep`: Recursively flatten and apply `processor` (e.g. `'1\\n2\\n3'`).

Example usage:
if isinstance(obj, list):
return _process_list(obj, _raw, joined_by=" ")

:param lst: A list to be modified.
:type lst: list
:param processor: The function that processes each item.
:type processor: callable
:param flatten: The level of flattening to apply. One of "none", "shallow", or "deep".
:type flatten: str
:param joined_by: If `flatten` is one of "shallow" or "deep", uses this string to join the elements of the list.
:param joined_by: str
:rtype: list | str
"""
match flatten:
case "shallow":
return joined_by.join(processor(item) for item in lst)
case "deep":
def _flatten_deep(x):
for item in x:
if isinstance(item, list):
yield from _flatten_deep(item)
else:
yield item

return joined_by.join(processor(item) for item in _flatten_deep(lst))
case _:
# for "none" and every other case
return [processor(item) for item in lst]

def _truncate(s, other):
def normalize(obj):
if isinstance(obj, list):
return _process_list(obj, str)
else:
return str(obj)

s, other = normalize(s), normalize(other)

def _raw(s):
"""Get raw representation of s, truncating if too long."""
if not config.dynamic_truncate:
if len(s) > config.truncate_len:
s = s[:config.truncate_len] + "..."
return s

if isinstance(s, list):
s = "\n".join(_raw(item) for item in s)
# find the index of first difference
limit = min(len(s), len(other))
i = limit
for index in range(limit):
if s[index] != other[index]:
i = index
break

# If the diff is within the first config.truncate_len characters,
# start from the beginning (no need for "..." at the start)
if i < config.truncate_len:
start = 0
end = min(config.truncate_len, len(s))
else:
# center around diff for differences further into the string
start = max(i - (config.truncate_len // 2), 0)
end = min(start + config.truncate_len, len(s))

snippet = s[start:end]

if start > 0:
snippet = "..." + snippet
if end < len(s):
snippet = snippet + "..."

return snippet


def _raw(s):
"""Get raw representation of s."""
if s == EOF:
return "EOF"
elif s == TIMEOUT:
return "TIMEOUT"

s = f'"{repr(str(s))[1:-1]}"'
if len(s) > 15:
s = s[:15] + "...\"" # Truncate if too long
return s


Expand Down
1 change: 1 addition & 0 deletions check50/assertions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .rewrite import rewrite, rewrite_enabled
Loading