From 7ac91a3c4d7ed59bbfdc48b7443828f9b850018a Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 24 Sep 2025 00:41:10 -0400 Subject: [PATCH] fix async --- doc/cp.txt | 4 ++ lua/cp/pickers/fzf_lua.lua | 6 +- lua/cp/pickers/init.lua | 26 +++++++- lua/cp/pickers/telescope.lua | 6 +- scrapers/__init__.py | 54 ++++++++++++---- scrapers/atcoder.py | 6 +- tests/scrapers/test_atcoder.py | 68 +++++++++++++++++++++ tests/scrapers/test_interface_compliance.py | 6 +- 8 files changed, 155 insertions(+), 21 deletions(-) diff --git a/doc/cp.txt b/doc/cp.txt index 6b06dd9..e4fc5a7 100644 --- a/doc/cp.txt +++ b/doc/cp.txt @@ -290,6 +290,10 @@ Usage examples: > for multi-test case problems commonly found in contests. + AtCoder Heuristic Contests (AHC) are excluded + from the contest list as they don't have + standard sample test cases. + Codeforces ~ *cp-codeforces* URL format: https://codeforces.com/contest/1234/problem/A diff --git a/lua/cp/pickers/fzf_lua.lua b/lua/cp/pickers/fzf_lua.lua index 8ca106d..d8acce9 100644 --- a/lua/cp/pickers/fzf_lua.lua +++ b/lua/cp/pickers/fzf_lua.lua @@ -8,9 +8,13 @@ local function problem_picker(platform, contest_id) if #problems == 0 then vim.notify( - ('No problems found for contest: %s %s'):format(platform_display_name, contest_id), + ("Contest %s %s hasn't started yet or has no available problems"):format( + platform_display_name, + contest_id + ), vim.log.levels.WARN ) + contest_picker(platform) return end diff --git a/lua/cp/pickers/init.lua b/lua/cp/pickers/init.lua index 947210a..f8cac85 100644 --- a/lua/cp/pickers/init.lua +++ b/lua/cp/pickers/init.lua @@ -59,6 +59,8 @@ local function get_contests_for_platform(platform) 'contests', } + logger.progress(('running: %s'):format(table.concat(cmd, ' '))) + local result = vim .system(cmd, { cwd = plugin_path, @@ -67,6 +69,11 @@ local function get_contests_for_platform(platform) }) :wait() + logger.progress(('exit code: %d, stdout length: %d'):format(result.code, #(result.stdout or ''))) + if result.stderr and #result.stderr > 0 then + logger.progress(('stderr: %s'):format(result.stderr:sub(1, 200))) + end + if result.code ~= 0 then logger.log( ('Failed to load contests: %s'):format(result.stderr or 'unknown error'), @@ -75,9 +82,18 @@ local function get_contests_for_platform(platform) return {} end + logger.progress(('stdout preview: %s'):format(result.stdout:sub(1, 100))) + local ok, data = pcall(vim.json.decode, result.stdout) - if not ok or not data.success then - logger.log('Failed to parse contest data', vim.log.levels.ERROR) + if not ok then + logger.log(('JSON parse error: %s'):format(tostring(data)), vim.log.levels.ERROR) + return {} + end + if not data.success then + logger.log( + ('Scraper returned success=false: %s'):format(data.error or 'no error message'), + vim.log.levels.ERROR + ) return {} end @@ -151,10 +167,14 @@ local function get_problems_for_contest(platform, contest_id) end local ok, data = pcall(vim.json.decode, result.stdout) - if not ok or not data.success then + if not ok then logger.log('Failed to parse contest data', vim.log.levels.ERROR) return problems end + if not data.success then + logger.log(data.error or 'Contest scraping failed', vim.log.levels.ERROR) + return problems + end if not data.problems or #data.problems == 0 then logger.log('Contest has no problems available', vim.log.levels.WARN) diff --git a/lua/cp/pickers/telescope.lua b/lua/cp/pickers/telescope.lua index 1417cc3..21350bd 100644 --- a/lua/cp/pickers/telescope.lua +++ b/lua/cp/pickers/telescope.lua @@ -13,9 +13,13 @@ local function problem_picker(opts, platform, contest_id) if #problems == 0 then vim.notify( - ('No problems found for contest: %s %s'):format(platform_display_name, contest_id), + ("Contest %s %s hasn't started yet or has no available problems"):format( + platform_display_name, + contest_id + ), vim.log.levels.WARN ) + contest_picker(opts, platform) return end diff --git a/scrapers/__init__.py b/scrapers/__init__.py index f0cfd45..2babd81 100644 --- a/scrapers/__init__.py +++ b/scrapers/__init__.py @@ -1,15 +1,45 @@ -from .atcoder import AtCoderScraper -from .base import BaseScraper, ScraperConfig -from .codeforces import CodeforcesScraper -from .cses import CSESScraper -from .models import ( - ContestListResult, - ContestSummary, - MetadataResult, - ProblemSummary, - TestCase, - TestsResult, -) +# Lazy imports to avoid module loading conflicts when running scrapers with -m +def __getattr__(name): + if name == "AtCoderScraper": + from .atcoder import AtCoderScraper + + return AtCoderScraper + elif name == "BaseScraper": + from .base import BaseScraper + + return BaseScraper + elif name == "ScraperConfig": + from .base import ScraperConfig + + return ScraperConfig + elif name == "CodeforcesScraper": + from .codeforces import CodeforcesScraper + + return CodeforcesScraper + elif name == "CSESScraper": + from .cses import CSESScraper + + return CSESScraper + elif name in [ + "ContestListResult", + "ContestSummary", + "MetadataResult", + "ProblemSummary", + "TestCase", + "TestsResult", + ]: + from .models import ( + ContestListResult, + ContestSummary, + MetadataResult, + ProblemSummary, + TestCase, + TestsResult, + ) + + return locals()[name] + raise AttributeError(f"module 'scrapers' has no attribute '{name}'") + __all__ = [ "AtCoderScraper", diff --git a/scrapers/atcoder.py b/scrapers/atcoder.py index 20cc3d3..cd72613 100644 --- a/scrapers/atcoder.py +++ b/scrapers/atcoder.py @@ -272,7 +272,11 @@ def scrape_contests() -> list[ContestSummary]: r"[\uff01-\uff5e]", lambda m: chr(ord(m.group()) - 0xFEE0), name ) - contests.append(ContestSummary(id=contest_id, name=name, display_name=name)) + # Skip AtCoder Heuristic Contests (AHC) as they don't have standard sample tests + if not contest_id.startswith("ahc"): + contests.append( + ContestSummary(id=contest_id, name=name, display_name=name) + ) return contests diff --git a/tests/scrapers/test_atcoder.py b/tests/scrapers/test_atcoder.py index dcde406..dc8b591 100644 --- a/tests/scrapers/test_atcoder.py +++ b/tests/scrapers/test_atcoder.py @@ -129,3 +129,71 @@ def test_scrape_contests_network_error(mocker): result = scrape_contests() assert result == [] + + +def test_scrape_contests_filters_ahc(mocker): + def mock_get_side_effect(url, **kwargs): + if url == "https://atcoder.jp/contests/archive": + mock_response = Mock() + mock_response.raise_for_status.return_value = None + mock_response.text = """ + + + + """ + return mock_response + elif "page=1" in url: + mock_response = Mock() + mock_response.raise_for_status.return_value = None + mock_response.text = """ + + + + + + + + + + + + + + + + + + + + + +
2025-01-15 21:00:00+0900AtCoder Beginner Contest 35001:40 - 1999
2025-01-14 21:00:00+0900AtCoder Heuristic Contest 04405:00-
2025-01-13 21:00:00+0900AtCoder Regular Contest 17002:001000 - 2799
+ """ + return mock_response + else: + mock_response = Mock() + mock_response.raise_for_status.return_value = None + mock_response.text = "" + return mock_response + + mocker.patch("scrapers.atcoder.requests.get", side_effect=mock_get_side_effect) + + result = scrape_contests() + + assert len(result) == 2 + assert result[0] == ContestSummary( + id="abc350", + name="AtCoder Beginner Contest 350", + display_name="AtCoder Beginner Contest 350", + ) + assert result[1] == ContestSummary( + id="arc170", + name="AtCoder Regular Contest 170", + display_name="AtCoder Regular Contest 170", + ) + + # Ensure ahc044 is filtered out + contest_ids = [contest.id for contest in result] + assert "ahc044" not in contest_ids diff --git a/tests/scrapers/test_interface_compliance.py b/tests/scrapers/test_interface_compliance.py index e81375b..a10c78c 100644 --- a/tests/scrapers/test_interface_compliance.py +++ b/tests/scrapers/test_interface_compliance.py @@ -8,9 +8,9 @@ from scrapers.base import BaseScraper from scrapers.models import ContestListResult, MetadataResult, TestsResult SCRAPERS = [ - cls - for name, cls in inspect.getmembers(scrapers, inspect.isclass) - if issubclass(cls, BaseScraper) and cls != BaseScraper + scrapers.AtCoderScraper, + scrapers.CodeforcesScraper, + scrapers.CSESScraper, ]