From 39c0c0ec1f394c36ebb105f058d87e3f102c7789 Mon Sep 17 00:00:00 2001 From: Paolo Lammens Date: Tue, 15 Sep 2020 15:33:32 +0100 Subject: [PATCH 1/8] BLD: refactor validate_unwanted_patterns.py Extract common code for checking a single file path. --- scripts/validate_unwanted_patterns.py | 45 +++++++++++++-------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/scripts/validate_unwanted_patterns.py b/scripts/validate_unwanted_patterns.py index b6ffab1482bbc..deba3984af4e6 100755 --- a/scripts/validate_unwanted_patterns.py +++ b/scripts/validate_unwanted_patterns.py @@ -437,16 +437,18 @@ def main( if not os.path.exists(source_path): raise ValueError("Please enter a valid path, pointing to a file/directory.") - is_failed: bool = False - file_path: str = "" - FILE_EXTENSIONS_TO_CHECK: FrozenSet[str] = frozenset( file_extensions_to_check.split(",") ) - PATHS_TO_IGNORE = frozenset(excluded_file_paths.split(",")) + PATHS_TO_IGNORE = frozenset( + os.path.abspath(os.path.normpath(path)) + for path in excluded_file_paths.split(",") + ) - if os.path.isfile(source_path): - file_path = source_path + is_failed: bool = False + + def check_file(file_path: str): + nonlocal is_failed with open(file_path) as file_obj: for line_number, msg in function(file_obj): is_failed = True @@ -456,24 +458,21 @@ def main( ) ) - for subdir, _, files in os.walk(source_path): - if any(path in subdir for path in PATHS_TO_IGNORE): - continue - for file_name in files: - if not any( - file_name.endswith(extension) for extension in FILE_EXTENSIONS_TO_CHECK - ): + if os.path.isfile(source_path): + check_file(source_path) + else: + for subdir, _, files in os.walk(source_path): + if any(path in subdir for path in PATHS_TO_IGNORE): continue - - file_path = os.path.join(subdir, file_name) - with open(file_path) as file_obj: - for line_number, msg in function(file_obj): - is_failed = True - print( - output_format.format( - source_path=file_path, line_number=line_number, msg=msg - ) - ) + for file_name in files: + if not any( + file_name.endswith(extension) + for extension in FILE_EXTENSIONS_TO_CHECK + ): + continue + + file_path = os.path.join(subdir, file_name) + check_file(file_path) return is_failed From ac8c5f7675305ecc33d4501f1b943c539f4a2db5 Mon Sep 17 00:00:00 2001 From: Paolo Lammens Date: Tue, 15 Sep 2020 15:50:39 +0100 Subject: [PATCH 2/8] BLD: fix ignored path checking in validate_unwanted_patterns.py The previous behaviour filtered out too many paths: any subdirectory whose relative path *contained* any of the ignored paths (which could be arbitrary strings) would be ignored. E.g., if PATHS_TO_IGNORE contained "foo", all of "./foo", "./spam/foo", "./spam/foo/eggs", "./barfoobaz", "./spam/foo.py" would get filtered out. On the other hand, individual files that *did* appear in the PAHTS_TO_IGNORE were *not* ignored. Now the behaviour should be a bit more robust. Ignored file pahts can be specified as relative paths or absolute paths (since they are all passed through os.path.abspath); any files below a subdirectory included in PATHS_TO_IGNORE will be filtered out, and so will any files which are explicitly mentioned in PATHS_TO_IGNORE. --- scripts/validate_unwanted_patterns.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/validate_unwanted_patterns.py b/scripts/validate_unwanted_patterns.py index deba3984af4e6..a287b9d3d8da5 100755 --- a/scripts/validate_unwanted_patterns.py +++ b/scripts/validate_unwanted_patterns.py @@ -458,20 +458,24 @@ def check_file(file_path: str): ) ) + def is_ignored(path: str): + path = os.path.abspath(os.path.normpath(path)) + return any(path.startswith(ignored_path) for ignored_path in PATHS_TO_IGNORE) + if os.path.isfile(source_path): check_file(source_path) else: for subdir, _, files in os.walk(source_path): - if any(path in subdir for path in PATHS_TO_IGNORE): + if is_ignored(subdir): continue for file_name in files: - if not any( + file_path = os.path.join(subdir, file_name) + if is_ignored(file_path) or not any( file_name.endswith(extension) for extension in FILE_EXTENSIONS_TO_CHECK ): continue - file_path = os.path.join(subdir, file_name) check_file(file_path) return is_failed From b3d98f6813e1a7282923d917770ffd1a4418b280 Mon Sep 17 00:00:00 2001 From: Paolo Lammens Date: Tue, 15 Sep 2020 16:42:10 +0100 Subject: [PATCH 3/8] BLD: allow multiple path arguments in validate_unwanted_patterns.py --- scripts/validate_unwanted_patterns.py | 47 +++++++++++++++------------ 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/scripts/validate_unwanted_patterns.py b/scripts/validate_unwanted_patterns.py index a287b9d3d8da5..91e07000583e1 100755 --- a/scripts/validate_unwanted_patterns.py +++ b/scripts/validate_unwanted_patterns.py @@ -16,7 +16,7 @@ import sys import token import tokenize -from typing import IO, Callable, FrozenSet, Iterable, List, Set, Tuple +from typing import IO, Callable, FrozenSet, Iterable, List, Sequence, Set, Tuple PRIVATE_IMPORTS_TO_IGNORE: Set[str] = { "_extension_array_shared_docs", @@ -403,7 +403,7 @@ def has_wrong_whitespace(first_line: str, second_line: str) -> bool: def main( function: Callable[[IO[str]], Iterable[Tuple[int, str]]], - source_path: str, + source_paths: Sequence[str], output_format: str, file_extensions_to_check: str, excluded_file_paths: str, @@ -415,8 +415,8 @@ def main( ---------- function : Callable Function to execute for the specified validation type. - source_path : str - Source path representing path to a file/directory. + source_paths : list of str + File paths of files and directories to check. output_format : str Output format of the error message. file_extensions_to_check : str @@ -434,7 +434,7 @@ def main( ValueError If the `source_path` is not pointing to existing file/directory. """ - if not os.path.exists(source_path): + if not all(os.path.exists(path) for path in source_paths): raise ValueError("Please enter a valid path, pointing to a file/directory.") FILE_EXTENSIONS_TO_CHECK: FrozenSet[str] = frozenset( @@ -462,21 +462,22 @@ def is_ignored(path: str): path = os.path.abspath(os.path.normpath(path)) return any(path.startswith(ignored_path) for ignored_path in PATHS_TO_IGNORE) - if os.path.isfile(source_path): - check_file(source_path) - else: - for subdir, _, files in os.walk(source_path): - if is_ignored(subdir): - continue - for file_name in files: - file_path = os.path.join(subdir, file_name) - if is_ignored(file_path) or not any( - file_name.endswith(extension) - for extension in FILE_EXTENSIONS_TO_CHECK - ): + for source_path in source_paths: + if os.path.isfile(source_path): + check_file(source_path) + else: + for subdir, _, files in os.walk(source_path): + if is_ignored(subdir): continue + for file_name in files: + file_path = os.path.join(subdir, file_name) + if is_ignored(file_path) or not any( + file_name.endswith(extension) + for extension in FILE_EXTENSIONS_TO_CHECK + ): + continue - check_file(file_path) + check_file(file_path) return is_failed @@ -493,7 +494,13 @@ def is_ignored(path: str): parser = argparse.ArgumentParser(description="Unwanted patterns checker.") parser.add_argument( - "path", nargs="?", default=".", help="Source path of file/directory to check." + "paths", + nargs="*", + default=["."], + help=( + "Source path(s) of files and directories to check. If a directory is " + "specified, all its contents are checked recursively." + ), ) parser.add_argument( "--format", @@ -524,7 +531,7 @@ def is_ignored(path: str): sys.exit( main( function=globals().get(args.validation_type), # type: ignore - source_path=args.path, + source_paths=args.paths, output_format=args.format, file_extensions_to_check=args.included_file_extensions, excluded_file_paths=args.excluded_file_paths, From d2dc5f7e60082c0cbd88c716a5150728ccd7d1ed Mon Sep 17 00:00:00 2001 From: Paolo Lammens Date: Tue, 15 Sep 2020 17:06:38 +0100 Subject: [PATCH 4/8] BLD: add verbose option to validate_unwanted_patterns.py --- scripts/validate_unwanted_patterns.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/scripts/validate_unwanted_patterns.py b/scripts/validate_unwanted_patterns.py index 91e07000583e1..40eda54de19e7 100755 --- a/scripts/validate_unwanted_patterns.py +++ b/scripts/validate_unwanted_patterns.py @@ -407,6 +407,7 @@ def main( output_format: str, file_extensions_to_check: str, excluded_file_paths: str, + verbose: int, ) -> bool: """ Main entry point of the script. @@ -423,6 +424,8 @@ def main( Comma separated values of what file extensions to check. excluded_file_paths : str Comma separated values of what file paths to exclude during the check. + verbose : int + Verbosity level (currently only distinguishes between zero and nonzero). Returns ------- @@ -449,14 +452,22 @@ def main( def check_file(file_path: str): nonlocal is_failed + local_is_failed = False + if verbose: + print(f"Checking {file_path}...", file=sys.stderr, end="") with open(file_path) as file_obj: for line_number, msg in function(file_obj): - is_failed = True + local_is_failed = True print( output_format.format( source_path=file_path, line_number=line_number, msg=msg ) ) + if not local_is_failed: + if verbose: + print(" OK", file=sys.stderr) + else: + is_failed = True def is_ignored(path: str): path = os.path.abspath(os.path.normpath(path)) @@ -525,6 +536,12 @@ def is_ignored(path: str): default="asv_bench/env", help="Comma separated file paths to exclude.", ) + parser.add_argument( + "-v", + "--verbose", + action="count", + help="Set the verbosity level to the number of times this flag is used.", + ) args = parser.parse_args() @@ -535,5 +552,6 @@ def is_ignored(path: str): output_format=args.format, file_extensions_to_check=args.included_file_extensions, excluded_file_paths=args.excluded_file_paths, + verbose=args.verbose, ) ) From 633abc47f23c983efa001c9d541f1aead498ba50 Mon Sep 17 00:00:00 2001 From: Paolo Lammens Date: Tue, 15 Sep 2020 17:45:45 +0100 Subject: [PATCH 5/8] BLD: add --no-override flag to validate_unwanted_patterns.py This flag controls whether individual files explicitly passed as arguments should override the --excluded-file-paths rule. --- scripts/validate_unwanted_patterns.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/scripts/validate_unwanted_patterns.py b/scripts/validate_unwanted_patterns.py index 40eda54de19e7..7b5b00159fcd5 100755 --- a/scripts/validate_unwanted_patterns.py +++ b/scripts/validate_unwanted_patterns.py @@ -407,6 +407,7 @@ def main( output_format: str, file_extensions_to_check: str, excluded_file_paths: str, + override: bool, verbose: int, ) -> bool: """ @@ -424,6 +425,9 @@ def main( Comma separated values of what file extensions to check. excluded_file_paths : str Comma separated values of what file paths to exclude during the check. + override: + Whether individual files mentioned in ``source_paths`` should override + ``excluded_file_paths``. verbose : int Verbosity level (currently only distinguishes between zero and nonzero). @@ -475,7 +479,8 @@ def is_ignored(path: str): for source_path in source_paths: if os.path.isfile(source_path): - check_file(source_path) + if override or not is_ignored(source_path): + check_file(source_path) else: for subdir, _, files in os.walk(source_path): if is_ignored(subdir): @@ -534,7 +539,20 @@ def is_ignored(path: str): parser.add_argument( "--excluded-file-paths", default="asv_bench/env", - help="Comma separated file paths to exclude.", + help=( + "Comma separated file paths to exclude. If an individual file is " + "explicitly passed in `paths`, it overrides this setting, unless the " + "--no-override flag is used." + ), + ) + parser.add_argument( + "--no-override", + dest="override", + action="store_false", + help=( + "Don't allow individual files explicitly mentioned in `pahts` to override " + "the excluded file paths (see --excluded-file-paths)." + ), ) parser.add_argument( "-v", @@ -552,6 +570,7 @@ def is_ignored(path: str): output_format=args.format, file_extensions_to_check=args.included_file_extensions, excluded_file_paths=args.excluded_file_paths, + override=args.override, verbose=args.verbose, ) ) From b70bc5d9d4a24ef731edf1094249678185139e3b Mon Sep 17 00:00:00 2001 From: Paolo Lammens Date: Tue, 15 Sep 2020 19:43:44 +0100 Subject: [PATCH 6/8] BLD: restrict code_checks.sh to tracked repo files Previously, some of the checks in code_checks.sh ran unrestricted on all the contents of the repository root (recursively), so that if any files extraneous to the repo were present (e.g. a virtual environment directory), they were checked too, potentially causing many false positives when a developer runs ./ci/code_checks.sh . The checker invocations that were already scoped (i.e. they were already restricted, in one way or another, to the actual pandas code, e.g. by restricting the search to the `pandas` subfolder) have been left as-is, while those that weren't are now given an explicit list of files that are tracked in the repo. --- ci/code_checks.sh | 53 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/ci/code_checks.sh b/ci/code_checks.sh index 54aa830379c07..c548b6f1ee855 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -25,6 +25,26 @@ BASE_DIR="$(dirname $0)/.." RET=0 CHECK=$1 + +# Get lists of files tracked by git: + +function quote_if_needed { + awk '{ print $0 ~ /.*\s+.*/ ? "\""$0"\"" : $0 }' +} + +function git_tracked_files { + [[ ! -z "$1" ]] && local patt="\\${1}$" || local patt="$" + local subdir=$2 + git ls-tree --name-only -r HEAD $subdir | grep -e $patt | quote_if_needed +} + +GIT_TRACKED_ALL=$(git_tracked_files) +GIT_TRACKED_ALL_PY_FILES=$(git_tracked_files .py) +GIT_TRACKED_DOCSOURCE_RST_FILES=$(git_tracked_files .rst doc/source) +GIT_TRACKED_REFERENCE_RST_FILES=$(git_tracked_files .rst doc/source/reference) +GIT_TRACKED_DEVELOPMENT_RST_FILES=$(git_tracked_files .rst doc/source/development) + + function invgrep { # grep with inverse exist status and formatting for azure-pipelines # @@ -38,6 +58,8 @@ function invgrep { return $((! $EXIT_STATUS)) } +export -f invgrep; # needed because of the use of xargs to pass in $GIT_TRACKED_ALL as args + if [[ "$GITHUB_ACTIONS" == "true" ]]; then FLAKE8_FORMAT="##[error]%(path)s:%(row)s:%(col)s:%(code)s:%(text)s" INVGREP_PREPEND="##[error]" @@ -52,7 +74,7 @@ if [[ -z "$CHECK" || "$CHECK" == "lint" ]]; then black --version MSG='Checking black formatting' ; echo $MSG - black . --check + echo $GIT_TRACKED_ALL_PY_FILES | xargs black --check RET=$(($RET + $?)) ; echo $MSG "DONE" # `setup.cfg` contains the list of error codes that are being ignored in flake8 @@ -62,7 +84,7 @@ if [[ -z "$CHECK" || "$CHECK" == "lint" ]]; then # pandas/_libs/src is C code, so no need to search there. MSG='Linting .py code' ; echo $MSG - flake8 --format="$FLAKE8_FORMAT" . + echo $GIT_TRACKED_ALL_PY_FILES | xargs flake8 --format="$FLAKE8_FORMAT" RET=$(($RET + $?)) ; echo $MSG "DONE" MSG='Linting .pyx and .pxd code' ; echo $MSG @@ -77,7 +99,7 @@ if [[ -z "$CHECK" || "$CHECK" == "lint" ]]; then flake8-rst --version MSG='Linting code-blocks in .rst documentation' ; echo $MSG - flake8-rst doc/source --filename=*.rst --format="$FLAKE8_FORMAT" + echo $GIT_TRACKED_DOCSOURCE_RST_FILES | xargs flake8-rst --format="$FLAKE8_FORMAT" RET=$(($RET + $?)) ; echo $MSG "DONE" # Check that cython casting is of the form `obj` as opposed to ` obj`; @@ -100,35 +122,38 @@ if [[ -z "$CHECK" || "$CHECK" == "lint" ]]; then cpplint --quiet --extensions=c,h --headers=h --recursive --filter=-readability/casting,-runtime/int,-build/include_subdir pandas/_libs/src/*.h pandas/_libs/src/parser pandas/_libs/ujson pandas/_libs/tslibs/src/datetime pandas/_libs/*.cpp RET=$(($RET + $?)) ; echo $MSG "DONE" + + VALIDATE_CMD=$BASE_DIR/scripts/validate_unwanted_patterns.py + MSG='Check for use of not concatenated strings' ; echo $MSG if [[ "$GITHUB_ACTIONS" == "true" ]]; then - $BASE_DIR/scripts/validate_unwanted_patterns.py --validation-type="strings_to_concatenate" --format="##[error]{source_path}:{line_number}:{msg}" . + echo $GIT_TRACKED_ALL_PY_FILES | xargs $VALIDATE_CMD --validation-type="strings_to_concatenate" --format="##[error]{source_path}:{line_number}:{msg}" --no-override else - $BASE_DIR/scripts/validate_unwanted_patterns.py --validation-type="strings_to_concatenate" . + echo $GIT_TRACKED_ALL_PY_FILES | xargs $VALIDATE_CMD --validation-type="strings_to_concatenate" --no-override fi RET=$(($RET + $?)) ; echo $MSG "DONE" MSG='Check for strings with wrong placed spaces' ; echo $MSG if [[ "$GITHUB_ACTIONS" == "true" ]]; then - $BASE_DIR/scripts/validate_unwanted_patterns.py --validation-type="strings_with_wrong_placed_whitespace" --format="##[error]{source_path}:{line_number}:{msg}" . + echo $GIT_TRACKED_ALL_PY_FILES | xargs $VALIDATE_CMD --validation-type="strings_with_wrong_placed_whitespace" --format="##[error]{source_path}:{line_number}:{msg}" --no-override else - $BASE_DIR/scripts/validate_unwanted_patterns.py --validation-type="strings_with_wrong_placed_whitespace" . + echo $GIT_TRACKED_ALL_PY_FILES | xargs $VALIDATE_CMD --validation-type="strings_with_wrong_placed_whitespace" --no-override fi RET=$(($RET + $?)) ; echo $MSG "DONE" MSG='Check for import of private attributes across modules' ; echo $MSG if [[ "$GITHUB_ACTIONS" == "true" ]]; then - $BASE_DIR/scripts/validate_unwanted_patterns.py --validation-type="private_import_across_module" --included-file-extensions="py" --excluded-file-paths=pandas/tests,asv_bench/,pandas/_vendored --format="##[error]{source_path}:{line_number}:{msg}" pandas/ + $VALIDATE_CMD --validation-type="private_import_across_module" --included-file-extensions="py" --excluded-file-paths=pandas/tests,asv_bench/,pandas/_vendored --format="##[error]{source_path}:{line_number}:{msg}" pandas/ else - $BASE_DIR/scripts/validate_unwanted_patterns.py --validation-type="private_import_across_module" --included-file-extensions="py" --excluded-file-paths=pandas/tests,asv_bench/,pandas/_vendored pandas/ + $VALIDATE_CMD --validation-type="private_import_across_module" --included-file-extensions="py" --excluded-file-paths=pandas/tests,asv_bench/,pandas/_vendored pandas/ fi RET=$(($RET + $?)) ; echo $MSG "DONE" MSG='Check for use of private functions across modules' ; echo $MSG if [[ "$GITHUB_ACTIONS" == "true" ]]; then - $BASE_DIR/scripts/validate_unwanted_patterns.py --validation-type="private_function_across_module" --included-file-extensions="py" --excluded-file-paths=pandas/tests,asv_bench/,pandas/_vendored,doc/ --format="##[error]{source_path}:{line_number}:{msg}" pandas/ + $VALIDATE_CMD --validation-type="private_function_across_module" --included-file-extensions="py" --excluded-file-paths=pandas/tests,asv_bench/,pandas/_vendored,doc/ --format="##[error]{source_path}:{line_number}:{msg}" pandas/ else - $BASE_DIR/scripts/validate_unwanted_patterns.py --validation-type="private_function_across_module" --included-file-extensions="py" --excluded-file-paths=pandas/tests,asv_bench/,pandas/_vendored,doc/ pandas/ + $VALIDATE_CMD --validation-type="private_function_across_module" --included-file-extensions="py" --excluded-file-paths=pandas/tests,asv_bench/,pandas/_vendored,doc/ pandas/ fi RET=$(($RET + $?)) ; echo $MSG "DONE" @@ -239,7 +264,7 @@ if [[ -z "$CHECK" || "$CHECK" == "patterns" ]]; then RET=$(($RET + $?)) ; echo $MSG "DONE" MSG='Check for extra blank lines after the class definition' ; echo $MSG - invgrep -R --include="*.py" --include="*.pyx" -E 'class.*:\n\n( )+"""' . + invgrep -R --include="*.py" --include="*.pyx" -E 'class.*:\n\n( )+"""' pandas RET=$(($RET + $?)) ; echo $MSG "DONE" MSG='Check for use of {foo!r} instead of {repr(foo)}' ; echo $MSG @@ -272,7 +297,7 @@ if [[ -z "$CHECK" || "$CHECK" == "patterns" ]]; then MSG='Check that no file in the repo contains trailing whitespaces' ; echo $MSG INVGREP_APPEND=" <- trailing whitespaces found" - invgrep -RI --exclude=\*.{svg,c,cpp,html,js} --exclude-dir=env "\s$" * + echo $GIT_TRACKED_ALL | xargs bash -c 'invgrep -RI "\s$" "$@"' _ RET=$(($RET + $?)) ; echo $MSG "DONE" unset INVGREP_APPEND fi @@ -390,7 +415,7 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then RET=$(($RET + $?)) ; echo $MSG "DONE" MSG='Validate correct capitalization among titles in documentation' ; echo $MSG - $BASE_DIR/scripts/validate_rst_title_capitalization.py $BASE_DIR/doc/source/development $BASE_DIR/doc/source/reference + echo "${GIT_TRACKED_REFERENCE_RST_FILES} ${GIT_TRACKED_DEVELOPMENT_RST_FILES}" | xargs $BASE_DIR/scripts/validate_rst_title_capitalization.py RET=$(($RET + $?)) ; echo $MSG "DONE" fi From 21feb5237bb6299629c89e4d4527013530099e23 Mon Sep 17 00:00:00 2001 From: Paolo Lammens Date: Tue, 15 Sep 2020 20:06:59 +0100 Subject: [PATCH 7/8] CLN: clean up new detected trailing whitespace --- pandas/_libs/src/ujson/lib/ultrajsonenc.c | 2 +- pandas/_libs/tslibs/src/datetime/np_datetime.c | 2 +- web/pandas/_templates/layout.html | 4 ++-- web/pandas/static/img/partners/r_studio.svg | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pandas/_libs/src/ujson/lib/ultrajsonenc.c b/pandas/_libs/src/ujson/lib/ultrajsonenc.c index 5343999c369f7..2af10a5b72d33 100644 --- a/pandas/_libs/src/ujson/lib/ultrajsonenc.c +++ b/pandas/_libs/src/ujson/lib/ultrajsonenc.c @@ -1134,7 +1134,7 @@ void encode(JSOBJ obj, JSONObjectEncoder *enc, const char *name, } break; - + } } diff --git a/pandas/_libs/tslibs/src/datetime/np_datetime.c b/pandas/_libs/tslibs/src/datetime/np_datetime.c index f647098140528..8eb995dee645b 100644 --- a/pandas/_libs/tslibs/src/datetime/np_datetime.c +++ b/pandas/_libs/tslibs/src/datetime/np_datetime.c @@ -312,7 +312,7 @@ int cmp_npy_datetimestruct(const npy_datetimestruct *a, * object into a NumPy npy_datetimestruct. Uses tzinfo (if present) * to convert to UTC time. * - * The following implementation just asks for attributes, and thus + * The following implementation just asks for attributes, and thus * supports datetime duck typing. The tzinfo time zone conversion * requires this style of access as well. * diff --git a/web/pandas/_templates/layout.html b/web/pandas/_templates/layout.html index 023bfe9e26b78..700c2dbab6dc9 100644 --- a/web/pandas/_templates/layout.html +++ b/web/pandas/_templates/layout.html @@ -33,7 +33,7 @@ {% if static.logo %}{% endif %} -