Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 15 additions & 6 deletions Lib/_colorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,21 +288,29 @@ def decolor(text: str) -> str:


def can_colorize(*, file: IO[str] | IO[bytes] | None = None) -> bool:

def _safe_getenv(k: str, fallback: str | None = None) -> str | None:
"""Exception-safe environment retrieval. See gh-128636."""
try:
return os.environ.get(k, fallback)
except Exception:
return fallback

if file is None:
file = sys.stdout

if not sys.flags.ignore_environment:
if os.environ.get("PYTHON_COLORS") == "0":
if _safe_getenv("PYTHON_COLORS") == "0":
return False
if os.environ.get("PYTHON_COLORS") == "1":
if _safe_getenv("PYTHON_COLORS") == "1":
return True
if os.environ.get("NO_COLOR"):
if _safe_getenv("NO_COLOR"):
return False
if not COLORIZE:
return False
if os.environ.get("FORCE_COLOR"):
if _safe_getenv("FORCE_COLOR"):
return True
if os.environ.get("TERM") == "dumb":
if _safe_getenv("TERM") == "dumb":
return False

if not hasattr(file, "fileno"):
Expand Down Expand Up @@ -345,7 +353,8 @@ def get_theme(
environment (including environment variable state and console configuration
on Windows) can also change in the course of the application life cycle.
"""
if force_color or (not force_no_color and can_colorize(file=tty_file)):
if force_color or (not force_no_color and
can_colorize(file=tty_file)):
return _theme
return theme_no_color

Expand Down
24 changes: 21 additions & 3 deletions Lib/_pydatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,11 @@ def fromisocalendar(cls, year, week, day):

@classmethod
def strptime(cls, date_string, format):
"""Parse string according to the given date format (like time.strptime())."""
"""Parse string according to the given date format (like time.strptime()).

For a list of supported format codes, see the documentation:
https://docs.python.org/3/library/datetime.html#format-codes
"""
import _strptime
return _strptime._strptime_datetime_date(cls, date_string, format)

Expand Down Expand Up @@ -1109,6 +1113,8 @@ def strftime(self, format):
Format using strftime().

Example: "%d/%m/%Y, %H:%M:%S"
For a list of supported format codes, see the documentation:
https://docs.python.org/3/library/datetime.html#format-codes
"""
return _wrap_strftime(self, format, self.timetuple())

Expand Down Expand Up @@ -1456,8 +1462,13 @@ def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold
return self

@classmethod

def strptime(cls, date_string, format):
"""Parse string according to the given time format (like time.strptime())."""
"""Parse string according to the given time format (like time.strptime()).

For a list of supported format codes, see the documentation:
https://docs.python.org/3/library/datetime.html#format-codes
"""
import _strptime
return _strptime._strptime_datetime_time(cls, date_string, format)

Expand Down Expand Up @@ -1650,6 +1661,9 @@ def fromisoformat(cls, time_string):
def strftime(self, format):
"""Format using strftime(). The date part of the timestamp passed
to underlying strftime should not be used.

For a list of supported format codes, see the documentation:
https://docs.python.org/3/library/datetime.html#format-codes
"""
# The year must be >= 1000 else Python's strftime implementation
# can raise a bogus exception.
Expand Down Expand Up @@ -2198,7 +2212,11 @@ def __str__(self):

@classmethod
def strptime(cls, date_string, format):
"""Parse string according to the given date and time format (like time.strptime())."""
"""Parse string according to the given time format (like time.strptime()).

For a list of supported format codes, see the documentation:
https://docs.python.org/3/library/datetime.html#format-codes
"""
import _strptime
return _strptime._strptime_datetime_datetime(cls, date_string, format)

Expand Down
31 changes: 29 additions & 2 deletions Lib/_pyrepl/_module_completer.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from __future__ import annotations

import importlib
import os
import pkgutil
import sys
import token
import tokenize
from importlib.machinery import FileFinder
from io import StringIO
from contextlib import contextmanager
from dataclasses import dataclass
Expand All @@ -16,6 +19,15 @@
from typing import Any, Iterable, Iterator, Mapping


HARDCODED_SUBMODULES = {
# Standard library submodules that are not detected by pkgutil.iter_modules
# but can be imported, so should be proposed in completion
"collections": ["abc"],
"os": ["path"],
"xml.parsers.expat": ["errors", "model"],
}


def make_default_module_completer() -> ModuleCompleter:
# Inside pyrepl, __package__ is set to None by default
return ModuleCompleter(namespace={'__package__': None})
Expand All @@ -41,6 +53,7 @@ def __init__(self, namespace: Mapping[str, Any] | None = None) -> None:
self.namespace = namespace or {}
self._global_cache: list[pkgutil.ModuleInfo] = []
self._curr_sys_path: list[str] = sys.path[:]
self._stdlib_path = os.path.dirname(importlib.__path__[0])

def get_completions(self, line: str) -> list[str] | None:
"""Return the next possible import completions for 'line'."""
Expand Down Expand Up @@ -95,12 +108,26 @@ def _find_modules(self, path: str, prefix: str) -> list[str]:
return []

modules: Iterable[pkgutil.ModuleInfo] = self.global_cache
is_stdlib_import: bool | None = None
for segment in path.split('.'):
modules = [mod_info for mod_info in modules
if mod_info.ispkg and mod_info.name == segment]
if is_stdlib_import is None:
# Top-level import decide if we import from stdlib or not
is_stdlib_import = all(
self._is_stdlib_module(mod_info) for mod_info in modules
)
modules = self.iter_submodules(modules)
return [module.name for module in modules
if self.is_suggestion_match(module.name, prefix)]

module_names = [module.name for module in modules]
if is_stdlib_import:
module_names.extend(HARDCODED_SUBMODULES.get(path, ()))
return [module_name for module_name in module_names
if self.is_suggestion_match(module_name, prefix)]

def _is_stdlib_module(self, module_info: pkgutil.ModuleInfo) -> bool:
return (isinstance(module_info.module_finder, FileFinder)
and module_info.module_finder.path == self._stdlib_path)

def is_suggestion_match(self, module_name: str, prefix: str) -> bool:
if prefix:
Expand Down
8 changes: 6 additions & 2 deletions Lib/_pyrepl/unix_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ def __init__(
self.pollob.register(self.input_fd, select.POLLIN)
self.terminfo = terminfo.TermInfo(term or None)
self.term = term
self.is_apple_terminal = (
platform.system() == "Darwin"
and os.getenv("TERM_PROGRAM") == "Apple_Terminal"
)

@overload
def _my_getstr(cap: str, optional: Literal[False] = False) -> bytes: ...
Expand Down Expand Up @@ -339,7 +343,7 @@ def prepare(self):
tcsetattr(self.input_fd, termios.TCSADRAIN, raw)

# In macOS terminal we need to deactivate line wrap via ANSI escape code
if platform.system() == "Darwin" and os.getenv("TERM_PROGRAM") == "Apple_Terminal":
if self.is_apple_terminal:
os.write(self.output_fd, b"\033[?7l")

self.screen = []
Expand Down Expand Up @@ -370,7 +374,7 @@ def restore(self):
self.flushoutput()
tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)

if platform.system() == "Darwin" and os.getenv("TERM_PROGRAM") == "Apple_Terminal":
if self.is_apple_terminal:
os.write(self.output_fd, b"\033[?7h")

if hasattr(self, "old_sigwinch"):
Expand Down
15 changes: 11 additions & 4 deletions Lib/_pyrepl/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ZERO_WIDTH_BRACKET = re.compile(r"\x01.*?\x02")
ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""})
IDENTIFIERS_AFTER = {"def", "class"}
KEYWORD_CONSTANTS = {"True", "False", "None"}
BUILTINS = {str(name) for name in dir(builtins) if not name.startswith('_')}


Expand Down Expand Up @@ -196,12 +197,12 @@ def gen_colors_from_token_stream(
is_def_name = False
span = Span.from_token(token, line_lengths)
yield ColorSpan(span, "definition")
elif token.string in ("True", "False", "None"):
span = Span.from_token(token, line_lengths)
yield ColorSpan(span, "keyword_constant")
elif keyword.iskeyword(token.string):
span_cls = "keyword"
if token.string in KEYWORD_CONSTANTS:
span_cls = "keyword_constant"
span = Span.from_token(token, line_lengths)
yield ColorSpan(span, "keyword")
yield ColorSpan(span, span_cls)
if token.string in IDENTIFIERS_AFTER:
is_def_name = True
elif (
Expand Down Expand Up @@ -263,6 +264,12 @@ def is_soft_keyword_used(*tokens: TI | None) -> bool:
return True
case (TI(string="case"), TI(string="_"), TI(string=":")):
return True
case (
None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":"),
TI(string="type"),
TI(T.NAME, string=s)
):
return not keyword.iskeyword(s)
case _:
return False

Expand Down
2 changes: 1 addition & 1 deletion Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2898,7 +2898,7 @@ def force_color(color: bool):
from .os_helper import EnvironmentVarGuard

with (
swap_attr(_colorize, "can_colorize", lambda file=None: color),
swap_attr(_colorize, "can_colorize", lambda *, file=None: color),
EnvironmentVarGuard() as env,
):
env.unset("FORCE_COLOR", "NO_COLOR", "PYTHON_COLORS")
Expand Down
Loading
Loading