Compare commits
26 commits
chore/add-
...
fix/async-
| Author | SHA1 | Date | |
|---|---|---|---|
| 6abe02f5ee | |||
| c4d410d290 | |||
| 622620f6d0 | |||
| 976838d981 | |||
| 06f72bbe2b | |||
| 6045042dfb | |||
| c192afc5d7 | |||
| b6f3398bbc | |||
| e02a29bd40 | |||
| 0f9715298e | |||
| 2148d9bd07 | |||
| 1162e7046b | |||
| b36ffba63a | |||
| 04d0c124cf | |||
| da433068ef | |||
| 51504b0121 | |||
| 49df7e015d | |||
| 029ea125b9 | |||
|
|
43193c3762 | ||
| de2bc07532 | |||
|
|
041e09ac04 | ||
| d23b4e59d1 | |||
|
|
19e71ac7fa | ||
| a54a06f939 | |||
|
|
b2c7f16890 | ||
| 276241447c |
23 changed files with 674 additions and 1727 deletions
3
.envrc
3
.envrc
|
|
@ -1,3 +0,0 @@
|
||||||
VIRTUAL_ENV="$PWD/.venv"
|
|
||||||
PATH_add "$VIRTUAL_ENV/bin"
|
|
||||||
export VIRTUAL_ENV
|
|
||||||
112
.github/workflows/ci.yaml
vendored
112
.github/workflows/ci.yaml
vendored
|
|
@ -1,112 +0,0 @@
|
||||||
name: ci
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
changes:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
lua: ${{ steps.changes.outputs.lua }}
|
|
||||||
python: ${{ steps.changes.outputs.python }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: dorny/paths-filter@v3
|
|
||||||
id: changes
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
lua:
|
|
||||||
- 'lua/**'
|
|
||||||
- 'spec/**'
|
|
||||||
- 'plugin/**'
|
|
||||||
- 'after/**'
|
|
||||||
- 'ftdetect/**'
|
|
||||||
- '*.lua'
|
|
||||||
- '.luarc.json'
|
|
||||||
- 'stylua.toml'
|
|
||||||
- 'selene.toml'
|
|
||||||
python:
|
|
||||||
- 'scripts/**'
|
|
||||||
- 'scrapers/**'
|
|
||||||
- 'tests/**'
|
|
||||||
- 'pyproject.toml'
|
|
||||||
- 'uv.lock'
|
|
||||||
|
|
||||||
lua-format:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: changes
|
|
||||||
if: ${{ needs.changes.outputs.lua == 'true' }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: JohnnyMorganz/stylua-action@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
version: 2.1.0
|
|
||||||
args: --check .
|
|
||||||
|
|
||||||
lua-lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: changes
|
|
||||||
if: ${{ needs.changes.outputs.lua == 'true' }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: NTBBloodbath/selene-action@v1.0.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
args: --display-style quiet .
|
|
||||||
|
|
||||||
lua-typecheck:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: changes
|
|
||||||
if: ${{ needs.changes.outputs.lua == 'true' }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: mrcjkb/lua-typecheck-action@v0
|
|
||||||
with:
|
|
||||||
checklevel: Warning
|
|
||||||
directories: lua
|
|
||||||
configpath: .luarc.json
|
|
||||||
|
|
||||||
python-format:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: changes
|
|
||||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: astral-sh/setup-uv@v4
|
|
||||||
- run: uv tool install ruff
|
|
||||||
- run: ruff format --check .
|
|
||||||
|
|
||||||
python-lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: changes
|
|
||||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: astral-sh/setup-uv@v4
|
|
||||||
- run: uv tool install ruff
|
|
||||||
- run: ruff check .
|
|
||||||
|
|
||||||
python-typecheck:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: changes
|
|
||||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: astral-sh/setup-uv@v4
|
|
||||||
- run: uv sync --dev
|
|
||||||
- run: uvx ty check .
|
|
||||||
|
|
||||||
python-test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: changes
|
|
||||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: astral-sh/setup-uv@v4
|
|
||||||
- run: uv sync --dev
|
|
||||||
- run: uv run camoufox fetch
|
|
||||||
- run: uv run pytest tests/ -v
|
|
||||||
4
.github/workflows/test.yaml
vendored
4
.github/workflows/test.yaml
vendored
|
|
@ -44,9 +44,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v4
|
uses: astral-sh/setup-uv@v4
|
||||||
- name: Install dependencies with pytest
|
- name: Install dependencies
|
||||||
run: uv sync --dev
|
run: uv sync --dev
|
||||||
- name: Fetch camoufox data
|
|
||||||
run: uv run camoufox fetch
|
|
||||||
- name: Run Python tests
|
- name: Run Python tests
|
||||||
run: uv run pytest tests/ -v
|
run: uv run pytest tests/ -v
|
||||||
|
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -14,3 +14,6 @@ __pycache__
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
.envrc
|
||||||
|
.direnv/
|
||||||
|
|
|
||||||
479
doc/cp.nvim.txt
479
doc/cp.nvim.txt
|
|
@ -18,6 +18,243 @@ REQUIREMENTS *cp-requirements*
|
||||||
- Unix-like operating system
|
- Unix-like operating system
|
||||||
- uv package manager (https://docs.astral.sh/uv/)
|
- uv package manager (https://docs.astral.sh/uv/)
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
SETUP *cp-setup*
|
||||||
|
|
||||||
|
Load cp.nvim with your package manager. For example, with lazy.nvim: >lua
|
||||||
|
{ 'barrettruth/cp.nvim' }
|
||||||
|
<
|
||||||
|
The plugin works automatically with no configuration required. For
|
||||||
|
customization, see |cp-config|.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
CONFIGURATION *cp-config*
|
||||||
|
|
||||||
|
Configuration is done via `vim.g.cp`. Set this before using the plugin:
|
||||||
|
>lua
|
||||||
|
vim.g.cp = {
|
||||||
|
languages = {
|
||||||
|
cpp = {
|
||||||
|
extension = 'cc',
|
||||||
|
commands = {
|
||||||
|
build = { 'g++', '-std=c++17', '{source}', '-o', '{binary}',
|
||||||
|
'-fdiagnostics-color=always' },
|
||||||
|
run = { '{binary}' },
|
||||||
|
debug = { 'g++', '-std=c++17', '-fsanitize=address,undefined',
|
||||||
|
'{source}', '-o', '{binary}' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
python = {
|
||||||
|
extension = 'py',
|
||||||
|
commands = {
|
||||||
|
run = { 'python', '{source}' },
|
||||||
|
debug = { 'python', '{source}' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
platforms = {
|
||||||
|
cses = {
|
||||||
|
enabled_languages = { 'cpp', 'python' },
|
||||||
|
default_language = 'cpp',
|
||||||
|
overrides = {
|
||||||
|
cpp = { extension = 'cpp', commands = { build = { ... } } }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
atcoder = {
|
||||||
|
enabled_languages = { 'cpp', 'python' },
|
||||||
|
default_language = 'cpp',
|
||||||
|
},
|
||||||
|
codeforces = {
|
||||||
|
enabled_languages = { 'cpp', 'python' },
|
||||||
|
default_language = 'cpp',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
open_url = true,
|
||||||
|
debug = false,
|
||||||
|
ui = {
|
||||||
|
ansi = true,
|
||||||
|
run = {
|
||||||
|
width = 0.3,
|
||||||
|
next_test_key = '<c-n>', -- or nil to disable
|
||||||
|
prev_test_key = '<c-p>', -- or nil to disable
|
||||||
|
},
|
||||||
|
panel = {
|
||||||
|
diff_modes = { 'side-by-side', 'git', 'vim' },
|
||||||
|
max_output_lines = 50,
|
||||||
|
},
|
||||||
|
diff = {
|
||||||
|
git = {
|
||||||
|
args = { 'diff', '--no-index', '--word-diff=plain',
|
||||||
|
'--word-diff-regex=.', '--no-prefix' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
picker = 'telescope',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
<
|
||||||
|
|
||||||
|
By default, C++ (g++ with ISO C++17) and Python are preconfigured under
|
||||||
|
'languages'. Platforms select which languages are enabled and which one is
|
||||||
|
the default; per-platform overrides can tweak 'extension' or 'commands'.
|
||||||
|
|
||||||
|
For example, to run CodeForces contests with Python by default:
|
||||||
|
>lua
|
||||||
|
vim.g.cp = {
|
||||||
|
platforms = {
|
||||||
|
codeforces = {
|
||||||
|
default_language = 'python',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
<
|
||||||
|
Any language is supported provided the proper configuration. For example, to
|
||||||
|
run CSES problems with Rust using the single schema:
|
||||||
|
>lua
|
||||||
|
vim.g.cp = {
|
||||||
|
languages = {
|
||||||
|
rust = {
|
||||||
|
extension = 'rs',
|
||||||
|
commands = {
|
||||||
|
build = { 'rustc', '{source}', '-o', '{binary}' },
|
||||||
|
run = { '{binary}' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
platforms = {
|
||||||
|
cses = {
|
||||||
|
enabled_languages = { 'cpp', 'python', 'rust' },
|
||||||
|
default_language = 'rust',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
<
|
||||||
|
*cp.Config*
|
||||||
|
Fields: ~
|
||||||
|
{languages} (table<string,|CpLanguage|>) Global language registry.
|
||||||
|
Each language provides an {extension} and {commands}.
|
||||||
|
{platforms} (table<string,|CpPlatform|>) Per-platform enablement,
|
||||||
|
default language, and optional overrides.
|
||||||
|
{hooks} (|cp.Hooks|) Hook functions called at various stages.
|
||||||
|
{debug} (boolean, default: false) Show info messages.
|
||||||
|
{scrapers} (string[]) Supported platform ids.
|
||||||
|
{filename} (function, optional)
|
||||||
|
function(contest, contest_id, problem_id, config, language): string
|
||||||
|
Should return full filename with extension.
|
||||||
|
(default: concatenates contest_id and problem_id, lowercased)
|
||||||
|
{ui} (|CpUI|) UI settings: panel, diff backend, picker.
|
||||||
|
{open_url} (boolean) Open the contest & problem url in the browser
|
||||||
|
when the contest is first opened.
|
||||||
|
|
||||||
|
*CpPlatform*
|
||||||
|
Fields: ~
|
||||||
|
{enabled_languages} (string[]) Language ids enabled on this platform.
|
||||||
|
{default_language} (string) One of {enabled_languages}.
|
||||||
|
{overrides} (table<string,|CpPlatformOverrides|>, optional)
|
||||||
|
Per-language overrides of {extension} and/or {commands}.
|
||||||
|
|
||||||
|
*CpLanguage*
|
||||||
|
Fields: ~
|
||||||
|
{extension} (string) File extension without leading dot.
|
||||||
|
{commands} (|CpLangCommands|) Command templates.
|
||||||
|
|
||||||
|
*CpLangCommands*
|
||||||
|
Fields: ~
|
||||||
|
{build} (string[], optional) For compiled languages.
|
||||||
|
Must include {source} and {binary}.
|
||||||
|
{run} (string[], optional) Runtime command.
|
||||||
|
Compiled: must include {binary}.
|
||||||
|
Interpreted: must include {source}.
|
||||||
|
{debug} (string[], optional) Debug variant; same token rules
|
||||||
|
as {build} (compiled) or {run} (interpreted).
|
||||||
|
|
||||||
|
*CpUI*
|
||||||
|
Fields: ~
|
||||||
|
{ansi} (boolean, default: true) Enable ANSI color parsing
|
||||||
|
and highlighting in both I/O view and panel.
|
||||||
|
{run} (|RunConfig|) I/O view configuration.
|
||||||
|
{panel} (|PanelConfig|) Test panel behavior configuration.
|
||||||
|
{diff} (|DiffConfig|) Diff backend configuration.
|
||||||
|
{picker} (string|nil) 'telescope', 'fzf-lua', or nil.
|
||||||
|
|
||||||
|
*RunConfig*
|
||||||
|
Fields: ~
|
||||||
|
{width} (number, default: 0.3) Width of I/O view splits as
|
||||||
|
fraction of screen (0.0 to 1.0).
|
||||||
|
{next_test_key} (string|nil, default: '<c-n>') Keymap to navigate
|
||||||
|
to next test in I/O view. Set to nil to disable.
|
||||||
|
{prev_test_key} (string|nil, default: '<c-p>') Keymap to navigate
|
||||||
|
to previous test in I/O view. Set to nil to disable.
|
||||||
|
{format_verdict} (|VerdictFormatter|, default: nil) Custom verdict line
|
||||||
|
formatter. See |cp-verdict-format|.
|
||||||
|
|
||||||
|
*EditConfig*
|
||||||
|
Fields: ~
|
||||||
|
{next_test_key} (string|nil, default: ']t') Jump to next test.
|
||||||
|
{prev_test_key} (string|nil, default: '[t') Jump to previous test.
|
||||||
|
{delete_test_key} (string|nil, default: 'gd') Delete current test.
|
||||||
|
{add_test_key} (string|nil, default: 'ga') Add new test.
|
||||||
|
{save_and_exit_key} (string|nil, default: 'q') Save and exit editor.
|
||||||
|
All keys are nil-able. Set to nil to disable.
|
||||||
|
|
||||||
|
*cp.PanelConfig*
|
||||||
|
Fields: ~
|
||||||
|
{diff_modes} (string[], default: {'side-by-side', 'git', 'vim'})
|
||||||
|
List of diff modes to cycle through with 't' key.
|
||||||
|
First element is the default mode.
|
||||||
|
Valid modes: 'side-by-side', 'git', 'vim'.
|
||||||
|
{max_output_lines} (number, default: 50) Maximum lines of test output.
|
||||||
|
|
||||||
|
*cp.DiffConfig*
|
||||||
|
Fields: ~
|
||||||
|
{git} (|cp.DiffGitConfig|) Git diff backend configuration.
|
||||||
|
|
||||||
|
*cp.DiffGitConfig*
|
||||||
|
Fields: ~
|
||||||
|
{args} (string[]) Command-line arguments for git diff.
|
||||||
|
Default: { 'diff', '--no-index', '--word-diff=plain',
|
||||||
|
'--word-diff-regex=.', '--no-prefix' }
|
||||||
|
• --no-index: Compare files outside git repository
|
||||||
|
• --word-diff=plain: Character-level diff markers
|
||||||
|
• --word-diff-regex=.: Split on every character
|
||||||
|
• --no-prefix: Remove a/ b/ prefixes from output
|
||||||
|
|
||||||
|
*cp.Hooks*
|
||||||
|
Fields: ~
|
||||||
|
{before_run} (function, optional) Called before test panel opens.
|
||||||
|
function(state: cp.State)
|
||||||
|
{before_debug} (function, optional) Called before debug build/run.
|
||||||
|
function(state: cp.State)
|
||||||
|
{setup_code} (function, optional) Called after source file is opened.
|
||||||
|
function(state: cp.State)
|
||||||
|
{setup_io_input} (function, optional) Called when I/O input buffer created.
|
||||||
|
function(bufnr: integer, state: cp.State)
|
||||||
|
Default: helpers.clearcol (removes line numbers/columns)
|
||||||
|
{setup_io_output} (function, optional) Called when I/O output buffer created.
|
||||||
|
function(bufnr: integer, state: cp.State)
|
||||||
|
Default: helpers.clearcol (removes line numbers/columns)
|
||||||
|
|
||||||
|
Hook functions receive the cp.nvim state object (|cp.State|). See
|
||||||
|
|lua/cp/state.lua| for available methods and fields.
|
||||||
|
|
||||||
|
The I/O buffer hooks are called once when the buffers are first created
|
||||||
|
during problem setup. Use these to customize buffer appearance (e.g.,
|
||||||
|
remove line numbers, set custom options). Access helpers via:
|
||||||
|
>lua
|
||||||
|
local helpers = require('cp').helpers
|
||||||
|
<
|
||||||
|
Example usage:
|
||||||
|
>lua
|
||||||
|
hooks = {
|
||||||
|
setup_code = function(state)
|
||||||
|
print("Setting up " .. state.get_base_name())
|
||||||
|
print("Source file: " .. state.get_source_file())
|
||||||
|
end,
|
||||||
|
setup_io_input = function(bufnr, state)
|
||||||
|
vim.api.nvim_set_option_value('number', false, { buf = bufnr })
|
||||||
|
end
|
||||||
|
}
|
||||||
|
<
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
COMMANDS *cp-commands*
|
COMMANDS *cp-commands*
|
||||||
|
|
||||||
|
|
@ -203,232 +440,40 @@ Debug Builds ~
|
||||||
<
|
<
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
CONFIGURATION *cp-config*
|
MAPPINGS *cp-mappings*
|
||||||
|
|
||||||
Configuration is done via `vim.g.cp_config`. Set this before using the plugin:
|
cp.nvim provides <Plug> mappings for all primary actions. These dispatch
|
||||||
>lua
|
through the same code path as |:CP|.
|
||||||
vim.g.cp_config = {
|
|
||||||
languages = {
|
|
||||||
cpp = {
|
|
||||||
extension = 'cc',
|
|
||||||
commands = {
|
|
||||||
build = { 'g++', '-std=c++17', '{source}', '-o', '{binary}',
|
|
||||||
'-fdiagnostics-color=always' },
|
|
||||||
run = { '{binary}' },
|
|
||||||
debug = { 'g++', '-std=c++17', '-fsanitize=address,undefined',
|
|
||||||
'{source}', '-o', '{binary}' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
python = {
|
|
||||||
extension = 'py',
|
|
||||||
commands = {
|
|
||||||
run = { 'python', '{source}' },
|
|
||||||
debug = { 'python', '{source}' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
platforms = {
|
|
||||||
cses = {
|
|
||||||
enabled_languages = { 'cpp', 'python' },
|
|
||||||
default_language = 'cpp',
|
|
||||||
overrides = {
|
|
||||||
cpp = { extension = 'cpp', commands = { build = { ... } } }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
atcoder = {
|
|
||||||
enabled_languages = { 'cpp', 'python' },
|
|
||||||
default_language = 'cpp',
|
|
||||||
},
|
|
||||||
codeforces = {
|
|
||||||
enabled_languages = { 'cpp', 'python' },
|
|
||||||
default_language = 'cpp',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
open_url = true,
|
|
||||||
debug = false,
|
|
||||||
ui = {
|
|
||||||
ansi = true,
|
|
||||||
run = {
|
|
||||||
width = 0.3,
|
|
||||||
next_test_key = '<c-n>', -- or nil to disable
|
|
||||||
prev_test_key = '<c-p>', -- or nil to disable
|
|
||||||
},
|
|
||||||
panel = {
|
|
||||||
diff_modes = { 'side-by-side', 'git', 'vim' },
|
|
||||||
max_output_lines = 50,
|
|
||||||
},
|
|
||||||
diff = {
|
|
||||||
git = {
|
|
||||||
args = { 'diff', '--no-index', '--word-diff=plain',
|
|
||||||
'--word-diff-regex=.', '--no-prefix' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
picker = 'telescope',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
<
|
|
||||||
|
|
||||||
By default, C++ (g++ with ISO C++17) and Python are preconfigured under
|
*<Plug>(cp-run)*
|
||||||
'languages'. Platforms select which languages are enabled and which one is
|
<Plug>(cp-run) Run tests in I/O view. Equivalent to :CP run.
|
||||||
the default; per-platform overrides can tweak 'extension' or 'commands'.
|
|
||||||
|
|
||||||
For example, to run CodeForces contests with Python by default:
|
*<Plug>(cp-panel)*
|
||||||
>lua
|
<Plug>(cp-panel) Open full-screen test panel. Equivalent to :CP panel.
|
||||||
vim.g.cp_config = {
|
|
||||||
platforms = {
|
|
||||||
codeforces = {
|
|
||||||
default_language = 'python',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
<
|
|
||||||
Any language is supported provided the proper configuration. For example, to
|
|
||||||
run CSES problems with Rust using the single schema:
|
|
||||||
>lua
|
|
||||||
vim.g.cp_config = {
|
|
||||||
languages = {
|
|
||||||
rust = {
|
|
||||||
extension = 'rs',
|
|
||||||
commands = {
|
|
||||||
build = { 'rustc', '{source}', '-o', '{binary}' },
|
|
||||||
run = { '{binary}' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
platforms = {
|
|
||||||
cses = {
|
|
||||||
enabled_languages = { 'cpp', 'python', 'rust' },
|
|
||||||
default_language = 'rust',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
<
|
|
||||||
*cp.Config*
|
|
||||||
Fields: ~
|
|
||||||
{languages} (table<string,|CpLanguage|>) Global language registry.
|
|
||||||
Each language provides an {extension} and {commands}.
|
|
||||||
{platforms} (table<string,|CpPlatform|>) Per-platform enablement,
|
|
||||||
default language, and optional overrides.
|
|
||||||
{hooks} (|cp.Hooks|) Hook functions called at various stages.
|
|
||||||
{debug} (boolean, default: false) Show info messages.
|
|
||||||
{scrapers} (string[]) Supported platform ids.
|
|
||||||
{filename} (function, optional)
|
|
||||||
function(contest, contest_id, problem_id, config, language): string
|
|
||||||
Should return full filename with extension.
|
|
||||||
(default: concatenates contest_id and problem_id, lowercased)
|
|
||||||
{ui} (|CpUI|) UI settings: panel, diff backend, picker.
|
|
||||||
{open_url} (boolean) Open the contest & problem url in the browser
|
|
||||||
when the contest is first opened.
|
|
||||||
|
|
||||||
*CpPlatform*
|
*<Plug>(cp-edit)*
|
||||||
Fields: ~
|
<Plug>(cp-edit) Open the test case editor. Equivalent to :CP edit.
|
||||||
{enabled_languages} (string[]) Language ids enabled on this platform.
|
|
||||||
{default_language} (string) One of {enabled_languages}.
|
|
||||||
{overrides} (table<string,|CpPlatformOverrides|>, optional)
|
|
||||||
Per-language overrides of {extension} and/or {commands}.
|
|
||||||
|
|
||||||
*CpLanguage*
|
*<Plug>(cp-next)*
|
||||||
Fields: ~
|
<Plug>(cp-next) Navigate to the next problem. Equivalent to :CP next.
|
||||||
{extension} (string) File extension without leading dot.
|
|
||||||
{commands} (|CpLangCommands|) Command templates.
|
|
||||||
|
|
||||||
*CpLangCommands*
|
*<Plug>(cp-prev)*
|
||||||
Fields: ~
|
<Plug>(cp-prev) Navigate to the previous problem. Equivalent to :CP prev.
|
||||||
{build} (string[], optional) For compiled languages.
|
|
||||||
Must include {source} and {binary}.
|
|
||||||
{run} (string[], optional) Runtime command.
|
|
||||||
Compiled: must include {binary}.
|
|
||||||
Interpreted: must include {source}.
|
|
||||||
{debug} (string[], optional) Debug variant; same token rules
|
|
||||||
as {build} (compiled) or {run} (interpreted).
|
|
||||||
|
|
||||||
*CpUI*
|
*<Plug>(cp-pick)*
|
||||||
Fields: ~
|
<Plug>(cp-pick) Launch the contest picker. Equivalent to :CP pick.
|
||||||
{ansi} (boolean, default: true) Enable ANSI color parsing
|
|
||||||
and highlighting in both I/O view and panel.
|
|
||||||
{run} (|RunConfig|) I/O view configuration.
|
|
||||||
{panel} (|PanelConfig|) Test panel behavior configuration.
|
|
||||||
{diff} (|DiffConfig|) Diff backend configuration.
|
|
||||||
{picker} (string|nil) 'telescope', 'fzf-lua', or nil.
|
|
||||||
|
|
||||||
*RunConfig*
|
*<Plug>(cp-interact)*
|
||||||
Fields: ~
|
<Plug>(cp-interact) Open interactive mode. Equivalent to :CP interact.
|
||||||
{width} (number, default: 0.3) Width of I/O view splits as
|
|
||||||
fraction of screen (0.0 to 1.0).
|
|
||||||
{next_test_key} (string|nil, default: '<c-n>') Keymap to navigate
|
|
||||||
to next test in I/O view. Set to nil to disable.
|
|
||||||
{prev_test_key} (string|nil, default: '<c-p>') Keymap to navigate
|
|
||||||
to previous test in I/O view. Set to nil to disable.
|
|
||||||
{format_verdict} (|VerdictFormatter|, default: nil) Custom verdict line
|
|
||||||
formatter. See |cp-verdict-format|.
|
|
||||||
|
|
||||||
*EditConfig*
|
Example configuration: >lua
|
||||||
Fields: ~
|
vim.keymap.set('n', '<leader>cr', '<Plug>(cp-run)')
|
||||||
{next_test_key} (string|nil, default: ']t') Jump to next test.
|
vim.keymap.set('n', '<leader>cp', '<Plug>(cp-panel)')
|
||||||
{prev_test_key} (string|nil, default: '[t') Jump to previous test.
|
vim.keymap.set('n', '<leader>ce', '<Plug>(cp-edit)')
|
||||||
{delete_test_key} (string|nil, default: 'gd') Delete current test.
|
vim.keymap.set('n', '<leader>cn', '<Plug>(cp-next)')
|
||||||
{add_test_key} (string|nil, default: 'ga') Add new test.
|
vim.keymap.set('n', '<leader>cN', '<Plug>(cp-prev)')
|
||||||
{save_and_exit_key} (string|nil, default: 'q') Save and exit editor.
|
vim.keymap.set('n', '<leader>cc', '<Plug>(cp-pick)')
|
||||||
All keys are nil-able. Set to nil to disable.
|
vim.keymap.set('n', '<leader>ci', '<Plug>(cp-interact)')
|
||||||
|
|
||||||
*cp.PanelConfig*
|
|
||||||
Fields: ~
|
|
||||||
{diff_modes} (string[], default: {'side-by-side', 'git', 'vim'})
|
|
||||||
List of diff modes to cycle through with 't' key.
|
|
||||||
First element is the default mode.
|
|
||||||
Valid modes: 'side-by-side', 'git', 'vim'.
|
|
||||||
{max_output_lines} (number, default: 50) Maximum lines of test output.
|
|
||||||
|
|
||||||
*cp.DiffConfig*
|
|
||||||
Fields: ~
|
|
||||||
{git} (|cp.DiffGitConfig|) Git diff backend configuration.
|
|
||||||
|
|
||||||
*cp.DiffGitConfig*
|
|
||||||
Fields: ~
|
|
||||||
{args} (string[]) Command-line arguments for git diff.
|
|
||||||
Default: { 'diff', '--no-index', '--word-diff=plain',
|
|
||||||
'--word-diff-regex=.', '--no-prefix' }
|
|
||||||
• --no-index: Compare files outside git repository
|
|
||||||
• --word-diff=plain: Character-level diff markers
|
|
||||||
• --word-diff-regex=.: Split on every character
|
|
||||||
• --no-prefix: Remove a/ b/ prefixes from output
|
|
||||||
|
|
||||||
*cp.Hooks*
|
|
||||||
Fields: ~
|
|
||||||
{before_run} (function, optional) Called before test panel opens.
|
|
||||||
function(state: cp.State)
|
|
||||||
{before_debug} (function, optional) Called before debug build/run.
|
|
||||||
function(state: cp.State)
|
|
||||||
{setup_code} (function, optional) Called after source file is opened.
|
|
||||||
function(state: cp.State)
|
|
||||||
{setup_io_input} (function, optional) Called when I/O input buffer created.
|
|
||||||
function(bufnr: integer, state: cp.State)
|
|
||||||
Default: helpers.clearcol (removes line numbers/columns)
|
|
||||||
{setup_io_output} (function, optional) Called when I/O output buffer created.
|
|
||||||
function(bufnr: integer, state: cp.State)
|
|
||||||
Default: helpers.clearcol (removes line numbers/columns)
|
|
||||||
|
|
||||||
Hook functions receive the cp.nvim state object (|cp.State|). See
|
|
||||||
|lua/cp/state.lua| for available methods and fields.
|
|
||||||
|
|
||||||
The I/O buffer hooks are called once when the buffers are first created
|
|
||||||
during problem setup. Use these to customize buffer appearance (e.g.,
|
|
||||||
remove line numbers, set custom options). Access helpers via:
|
|
||||||
>lua
|
|
||||||
local helpers = require('cp').helpers
|
|
||||||
<
|
|
||||||
Example usage:
|
|
||||||
>lua
|
|
||||||
hooks = {
|
|
||||||
setup_code = function(state)
|
|
||||||
print("Setting up " .. state.get_base_name())
|
|
||||||
print("Source file: " .. state.get_source_file())
|
|
||||||
end,
|
|
||||||
setup_io_input = function(bufnr, state)
|
|
||||||
-- Custom setup for input buffer
|
|
||||||
vim.api.nvim_set_option_value('number', false, { buf = bufnr })
|
|
||||||
end
|
|
||||||
}
|
|
||||||
<
|
<
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
|
|
|
||||||
43
flake.lock
generated
Normal file
43
flake.lock
generated
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1771008912,
|
||||||
|
"narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "a82ccc39b39b621151d6732718e3e250109076fa",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"systems": "systems"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1689347949,
|
||||||
|
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default-linux",
|
||||||
|
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default-linux",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
72
flake.nix
Normal file
72
flake.nix
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
systems.url = "github:nix-systems/default-linux";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
systems,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
eachSystem = nixpkgs.lib.genAttrs (import systems);
|
||||||
|
pkgsFor = system: nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
|
mkPythonEnv =
|
||||||
|
pkgs:
|
||||||
|
pkgs.python312.withPackages (ps: [
|
||||||
|
ps.backoff
|
||||||
|
ps.beautifulsoup4
|
||||||
|
ps.curl-cffi
|
||||||
|
ps.httpx
|
||||||
|
ps.ndjson
|
||||||
|
ps.pydantic
|
||||||
|
ps.requests
|
||||||
|
]);
|
||||||
|
|
||||||
|
mkPlugin =
|
||||||
|
pkgs:
|
||||||
|
let
|
||||||
|
pythonEnv = mkPythonEnv pkgs;
|
||||||
|
in
|
||||||
|
pkgs.vimUtils.buildVimPlugin {
|
||||||
|
pname = "cp-nvim";
|
||||||
|
version = "0-unstable-${self.shortRev or self.dirtyShortRev or "dev"}";
|
||||||
|
src = self;
|
||||||
|
postPatch = ''
|
||||||
|
substituteInPlace lua/cp/utils.lua \
|
||||||
|
--replace-fail "local _nix_python = nil" \
|
||||||
|
"local _nix_python = '${pythonEnv.interpreter}'"
|
||||||
|
'';
|
||||||
|
nvimSkipModule = [
|
||||||
|
"cp.pickers.telescope"
|
||||||
|
"cp.version"
|
||||||
|
];
|
||||||
|
passthru = { inherit pythonEnv; };
|
||||||
|
meta.description = "Competitive programming plugin for Neovim";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
overlays.default = final: prev: {
|
||||||
|
vimPlugins = prev.vimPlugins // {
|
||||||
|
cp-nvim = mkPlugin final;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
packages = eachSystem (system: {
|
||||||
|
default = mkPlugin (pkgsFor system);
|
||||||
|
pythonEnv = mkPythonEnv (pkgsFor system);
|
||||||
|
});
|
||||||
|
|
||||||
|
devShells = eachSystem (system: {
|
||||||
|
default = (pkgsFor system).mkShell {
|
||||||
|
packages = with (pkgsFor system); [
|
||||||
|
uv
|
||||||
|
python312
|
||||||
|
];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -292,7 +292,15 @@ end
|
||||||
---@return cp.Config
|
---@return cp.Config
|
||||||
function M.setup(user_config)
|
function M.setup(user_config)
|
||||||
vim.validate({ user_config = { user_config, { 'table', 'nil' }, true } })
|
vim.validate({ user_config = { user_config, { 'table', 'nil' }, true } })
|
||||||
local cfg = vim.tbl_deep_extend('force', vim.deepcopy(M.defaults), user_config or {})
|
local defaults = vim.deepcopy(M.defaults)
|
||||||
|
if user_config and user_config.platforms then
|
||||||
|
for plat in pairs(defaults.platforms) do
|
||||||
|
if not user_config.platforms[plat] then
|
||||||
|
defaults.platforms[plat] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local cfg = vim.tbl_deep_extend('force', defaults, user_config or {})
|
||||||
|
|
||||||
if not next(cfg.languages) then
|
if not next(cfg.languages) then
|
||||||
error('[cp.nvim] At least one language must be configured')
|
error('[cp.nvim] At least one language must be configured')
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ local utils = require('cp.utils')
|
||||||
local function check()
|
local function check()
|
||||||
vim.health.start('cp.nvim [required] ~')
|
vim.health.start('cp.nvim [required] ~')
|
||||||
|
|
||||||
|
utils.setup_python_env()
|
||||||
|
|
||||||
if vim.fn.has('nvim-0.10.0') == 1 then
|
if vim.fn.has('nvim-0.10.0') == 1 then
|
||||||
vim.health.ok('Neovim 0.10.0+ detected')
|
vim.health.ok('Neovim 0.10.0+ detected')
|
||||||
else
|
else
|
||||||
|
|
@ -16,6 +18,16 @@ local function check()
|
||||||
vim.health.error('Windows is not supported')
|
vim.health.error('Windows is not supported')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if utils.is_nix_build() then
|
||||||
|
local source = utils.is_nix_discovered() and 'runtime discovery' or 'flake install'
|
||||||
|
vim.health.ok('Nix Python environment detected (' .. source .. ')')
|
||||||
|
local py = utils.get_nix_python()
|
||||||
|
vim.health.info('Python: ' .. py)
|
||||||
|
local r = vim.system({ py, '--version' }, { text = true }):wait()
|
||||||
|
if r.code == 0 then
|
||||||
|
vim.health.info('Python version: ' .. r.stdout:gsub('\n', ''))
|
||||||
|
end
|
||||||
|
else
|
||||||
if vim.fn.executable('uv') == 1 then
|
if vim.fn.executable('uv') == 1 then
|
||||||
vim.health.ok('uv executable found')
|
vim.health.ok('uv executable found')
|
||||||
local r = vim.system({ 'uv', '--version' }, { text = true }):wait()
|
local r = vim.system({ 'uv', '--version' }, { text = true }):wait()
|
||||||
|
|
@ -26,6 +38,10 @@ local function check()
|
||||||
vim.health.warn('uv not found (install https://docs.astral.sh/uv/ for scraping)')
|
vim.health.warn('uv not found (install https://docs.astral.sh/uv/ for scraping)')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if vim.fn.executable('nix') == 1 then
|
||||||
|
vim.health.info('nix available but Python environment not resolved via nix')
|
||||||
|
end
|
||||||
|
|
||||||
local plugin_path = utils.get_plugin_path()
|
local plugin_path = utils.get_plugin_path()
|
||||||
local venv_dir = plugin_path .. '/.venv'
|
local venv_dir = plugin_path .. '/.venv'
|
||||||
if vim.fn.isdirectory(venv_dir) == 1 then
|
if vim.fn.isdirectory(venv_dir) == 1 then
|
||||||
|
|
@ -33,6 +49,7 @@ local function check()
|
||||||
else
|
else
|
||||||
vim.health.info('Python virtual environment not set up (created on first scrape)')
|
vim.health.info('Python virtual environment not set up (created on first scrape)')
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local time_cap = utils.time_capability()
|
local time_cap = utils.time_capability()
|
||||||
if time_cap.ok then
|
if time_cap.ok then
|
||||||
|
|
@ -41,7 +58,7 @@ local function check()
|
||||||
vim.health.error('GNU time not found: ' .. (time_cap.reason or ''))
|
vim.health.error('GNU time not found: ' .. (time_cap.reason or ''))
|
||||||
end
|
end
|
||||||
|
|
||||||
local timeout_cap = utils.time_capability()
|
local timeout_cap = utils.timeout_capability()
|
||||||
if timeout_cap.ok then
|
if timeout_cap.ok then
|
||||||
vim.health.ok('GNU timeout found: ' .. timeout_cap.path)
|
vim.health.ok('GNU timeout found: ' .. timeout_cap.path)
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,25 @@ local initialized = false
|
||||||
|
|
||||||
local function ensure_initialized()
|
local function ensure_initialized()
|
||||||
if initialized then
|
if initialized then
|
||||||
return
|
return true
|
||||||
end
|
end
|
||||||
local user_config = vim.g.cp_config or {}
|
local user_config = vim.g.cp or {}
|
||||||
local config = config_module.setup(user_config)
|
local ok, result = pcall(config_module.setup, user_config)
|
||||||
config_module.set_current_config(config)
|
if not ok then
|
||||||
|
local msg = tostring(result):gsub('^.+:%d+: ', '')
|
||||||
|
vim.notify(msg, vim.log.levels.ERROR)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
config_module.set_current_config(result)
|
||||||
initialized = true
|
initialized = true
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return nil
|
---@return nil
|
||||||
function M.handle_command(opts)
|
function M.handle_command(opts)
|
||||||
ensure_initialized()
|
if not ensure_initialized() then
|
||||||
|
return
|
||||||
|
end
|
||||||
local commands = require('cp.commands')
|
local commands = require('cp.commands')
|
||||||
commands.handle_command(opts)
|
commands.handle_command(opts)
|
||||||
end
|
end
|
||||||
|
|
@ -34,4 +42,13 @@ function M.is_initialized()
|
||||||
return initialized
|
return initialized
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@deprecated Use `vim.g.cp` instead
|
||||||
|
function M.setup(user_config)
|
||||||
|
vim.deprecate('require("cp").setup()', 'vim.g.cp', 'v0.7.7', 'cp.nvim', false)
|
||||||
|
|
||||||
|
if user_config then
|
||||||
|
vim.g.cp = vim.tbl_deep_extend('force', vim.g.cp or {}, user_config)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ end
|
||||||
function M.compile(compile_cmd, substitutions, on_complete)
|
function M.compile(compile_cmd, substitutions, on_complete)
|
||||||
local cmd = substitute_template(compile_cmd, substitutions)
|
local cmd = substitute_template(compile_cmd, substitutions)
|
||||||
local sh = table.concat(cmd, ' ') .. ' 2>&1'
|
local sh = table.concat(cmd, ' ') .. ' 2>&1'
|
||||||
|
logger.log('compile: ' .. sh)
|
||||||
|
|
||||||
local t0 = vim.uv.hrtime()
|
local t0 = vim.uv.hrtime()
|
||||||
vim.system({ 'sh', '-c', sh }, { text = false }, function(r)
|
vim.system({ 'sh', '-c', sh }, { text = false }, function(r)
|
||||||
|
|
@ -119,6 +120,7 @@ function M.run(cmd, stdin, timeout_ms, memory_mb, on_complete)
|
||||||
local sec = math.ceil(timeout_ms / 1000)
|
local sec = math.ceil(timeout_ms / 1000)
|
||||||
local timeout_prefix = ('%s -k 1s %ds '):format(timeout_bin, sec)
|
local timeout_prefix = ('%s -k 1s %ds '):format(timeout_bin, sec)
|
||||||
local sh = prefix .. timeout_prefix .. ('%s -v sh -c %q 2>&1'):format(time_bin, prog)
|
local sh = prefix .. timeout_prefix .. ('%s -v sh -c %q 2>&1'):format(time_bin, prog)
|
||||||
|
logger.log('run: ' .. sh)
|
||||||
|
|
||||||
local t0 = vim.uv.hrtime()
|
local t0 = vim.uv.hrtime()
|
||||||
vim.system({ 'sh', '-c', sh }, { stdin = stdin, text = true }, function(r)
|
vim.system({ 'sh', '-c', sh }, { stdin = stdin, text = true }, function(r)
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,22 @@ end
|
||||||
---@param args string[]
|
---@param args string[]
|
||||||
---@param opts { sync?: boolean, ndjson?: boolean, on_event?: fun(ev: table), on_exit?: fun(result: table) }
|
---@param opts { sync?: boolean, ndjson?: boolean, on_event?: fun(ev: table), on_exit?: fun(result: table) }
|
||||||
local function run_scraper(platform, subcommand, args, opts)
|
local function run_scraper(platform, subcommand, args, opts)
|
||||||
|
if not utils.setup_python_env() then
|
||||||
|
local msg = 'no Python environment available (install uv or nix)'
|
||||||
|
logger.log(msg, vim.log.levels.ERROR)
|
||||||
|
if opts and opts.on_exit then
|
||||||
|
opts.on_exit({ success = false, error = msg })
|
||||||
|
end
|
||||||
|
return { success = false, error = msg }
|
||||||
|
end
|
||||||
|
|
||||||
local plugin_path = utils.get_plugin_path()
|
local plugin_path = utils.get_plugin_path()
|
||||||
local cmd = { 'uv', 'run', '--directory', plugin_path, '-m', 'scrapers.' .. platform, subcommand }
|
local cmd = utils.get_python_cmd(platform, plugin_path)
|
||||||
|
vim.list_extend(cmd, { subcommand })
|
||||||
vim.list_extend(cmd, args)
|
vim.list_extend(cmd, args)
|
||||||
|
|
||||||
|
logger.log('scraper cmd: ' .. table.concat(cmd, ' '))
|
||||||
|
|
||||||
local env = vim.fn.environ()
|
local env = vim.fn.environ()
|
||||||
env.VIRTUAL_ENV = ''
|
env.VIRTUAL_ENV = ''
|
||||||
env.PYTHONPATH = ''
|
env.PYTHONPATH = ''
|
||||||
|
|
@ -41,10 +53,12 @@ local function run_scraper(platform, subcommand, args, opts)
|
||||||
local buf = ''
|
local buf = ''
|
||||||
|
|
||||||
local handle
|
local handle
|
||||||
handle = uv.spawn(
|
handle = uv.spawn(cmd[1], {
|
||||||
cmd[1],
|
args = vim.list_slice(cmd, 2),
|
||||||
{ args = vim.list_slice(cmd, 2), stdio = { nil, stdout, stderr }, env = env },
|
stdio = { nil, stdout, stderr },
|
||||||
function(code, signal)
|
env = env,
|
||||||
|
cwd = plugin_path,
|
||||||
|
}, function(code, signal)
|
||||||
if buf ~= '' and opts.on_event then
|
if buf ~= '' and opts.on_event then
|
||||||
local ok_tail, ev_tail = pcall(vim.json.decode, buf)
|
local ok_tail, ev_tail = pcall(vim.json.decode, buf)
|
||||||
if ok_tail then
|
if ok_tail then
|
||||||
|
|
@ -64,8 +78,7 @@ local function run_scraper(platform, subcommand, args, opts)
|
||||||
if handle and not handle:is_closing() then
|
if handle and not handle:is_closing() then
|
||||||
handle:close()
|
handle:close()
|
||||||
end
|
end
|
||||||
end
|
end)
|
||||||
)
|
|
||||||
|
|
||||||
if not handle then
|
if not handle then
|
||||||
logger.log('Failed to start scraper process', vim.log.levels.ERROR)
|
logger.log('Failed to start scraper process', vim.log.levels.ERROR)
|
||||||
|
|
@ -102,7 +115,7 @@ local function run_scraper(platform, subcommand, args, opts)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local sysopts = { text = true, timeout = 30000, env = env }
|
local sysopts = { text = true, timeout = 30000, env = env, cwd = plugin_path }
|
||||||
if opts and opts.sync then
|
if opts and opts.sync then
|
||||||
local result = vim.system(cmd, sysopts):wait()
|
local result = vim.system(cmd, sysopts):wait()
|
||||||
return syshandle(result)
|
return syshandle(result)
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,8 @@ function M.setup_contest(platform, contest_id, problem_id, language)
|
||||||
vim.bo[bufnr].buftype = ''
|
vim.bo[bufnr].buftype = ''
|
||||||
vim.bo[bufnr].swapfile = false
|
vim.bo[bufnr].swapfile = false
|
||||||
|
|
||||||
|
state.set_language(lang)
|
||||||
|
|
||||||
if cfg.hooks and cfg.hooks.setup_code and not vim.b[bufnr].cp_setup_done then
|
if cfg.hooks and cfg.hooks.setup_code and not vim.b[bufnr].cp_setup_done then
|
||||||
local ok = pcall(cfg.hooks.setup_code, state)
|
local ok = pcall(cfg.hooks.setup_code, state)
|
||||||
if ok then
|
if ok then
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,14 @@ function M.toggle_interactive(interactor_cmd)
|
||||||
end
|
end
|
||||||
local orchestrator =
|
local orchestrator =
|
||||||
vim.fn.fnamemodify(utils.get_plugin_path() .. '/scripts/interact.py', ':p')
|
vim.fn.fnamemodify(utils.get_plugin_path() .. '/scripts/interact.py', ':p')
|
||||||
|
if utils.is_nix_build() then
|
||||||
|
cmdline = table.concat({
|
||||||
|
vim.fn.shellescape(utils.get_nix_python()),
|
||||||
|
vim.fn.shellescape(orchestrator),
|
||||||
|
vim.fn.shellescape(interactor),
|
||||||
|
vim.fn.shellescape(binary),
|
||||||
|
}, ' ')
|
||||||
|
else
|
||||||
cmdline = table.concat({
|
cmdline = table.concat({
|
||||||
'uv',
|
'uv',
|
||||||
'run',
|
'run',
|
||||||
|
|
@ -128,6 +136,7 @@ function M.toggle_interactive(interactor_cmd)
|
||||||
vim.fn.shellescape(interactor),
|
vim.fn.shellescape(interactor),
|
||||||
vim.fn.shellescape(binary),
|
vim.fn.shellescape(binary),
|
||||||
}, ' ')
|
}, ' ')
|
||||||
|
end
|
||||||
else
|
else
|
||||||
cmdline = vim.fn.shellescape(binary)
|
cmdline = vim.fn.shellescape(binary)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
142
lua/cp/utils.lua
142
lua/cp/utils.lua
|
|
@ -2,6 +2,9 @@ local M = {}
|
||||||
|
|
||||||
local logger = require('cp.log')
|
local logger = require('cp.log')
|
||||||
|
|
||||||
|
local _nix_python = nil
|
||||||
|
local _nix_discovered = false
|
||||||
|
|
||||||
local uname = vim.loop.os_uname()
|
local uname = vim.loop.os_uname()
|
||||||
|
|
||||||
local _time_cached = false
|
local _time_cached = false
|
||||||
|
|
@ -57,7 +60,11 @@ local function find_gnu_time()
|
||||||
|
|
||||||
_time_cached = true
|
_time_cached = true
|
||||||
_time_path = nil
|
_time_path = nil
|
||||||
|
if uname and uname.sysname == 'Darwin' then
|
||||||
|
_time_reason = 'GNU time not found (install via: brew install coreutils)'
|
||||||
|
else
|
||||||
_time_reason = 'GNU time not found'
|
_time_reason = 'GNU time not found'
|
||||||
|
end
|
||||||
return _time_path, _time_reason
|
return _time_path, _time_reason
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -79,27 +86,101 @@ function M.get_plugin_path()
|
||||||
return vim.fn.fnamemodify(plugin_path, ':h:h:h')
|
return vim.fn.fnamemodify(plugin_path, ':h:h:h')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return boolean
|
||||||
|
function M.is_nix_build()
|
||||||
|
return _nix_python ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return string|nil
|
||||||
|
function M.get_nix_python()
|
||||||
|
return _nix_python
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return boolean
|
||||||
|
function M.is_nix_discovered()
|
||||||
|
return _nix_discovered
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param module string
|
||||||
|
---@param plugin_path string
|
||||||
|
---@return string[]
|
||||||
|
function M.get_python_cmd(module, plugin_path)
|
||||||
|
if _nix_python then
|
||||||
|
return { _nix_python, '-m', 'scrapers.' .. module }
|
||||||
|
end
|
||||||
|
return { 'uv', 'run', '--directory', plugin_path, '-m', 'scrapers.' .. module }
|
||||||
|
end
|
||||||
|
|
||||||
local python_env_setup = false
|
local python_env_setup = false
|
||||||
|
|
||||||
|
---@return boolean
|
||||||
|
local function discover_nix_python()
|
||||||
|
local cache_dir = vim.fn.stdpath('cache') .. '/cp-nvim'
|
||||||
|
local cache_file = cache_dir .. '/nix-python'
|
||||||
|
|
||||||
|
local f = io.open(cache_file, 'r')
|
||||||
|
if f then
|
||||||
|
local cached = f:read('*l')
|
||||||
|
f:close()
|
||||||
|
if cached and vim.fn.executable(cached) == 1 then
|
||||||
|
_nix_python = cached
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local plugin_path = M.get_plugin_path()
|
||||||
|
vim.notify('[cp.nvim] Building Python environment with nix...', vim.log.levels.INFO)
|
||||||
|
vim.cmd.redraw()
|
||||||
|
local result = vim
|
||||||
|
.system(
|
||||||
|
{ 'nix', 'build', plugin_path .. '#pythonEnv', '--no-link', '--print-out-paths' },
|
||||||
|
{ text = true }
|
||||||
|
)
|
||||||
|
:wait()
|
||||||
|
|
||||||
|
if result.code ~= 0 then
|
||||||
|
logger.log('nix build #pythonEnv failed: ' .. (result.stderr or ''), vim.log.levels.WARN)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local store_path = result.stdout:gsub('%s+$', '')
|
||||||
|
local python_path = store_path .. '/bin/python3'
|
||||||
|
|
||||||
|
if vim.fn.executable(python_path) ~= 1 then
|
||||||
|
logger.log('nix python not executable at ' .. python_path, vim.log.levels.WARN)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.fn.mkdir(cache_dir, 'p')
|
||||||
|
f = io.open(cache_file, 'w')
|
||||||
|
if f then
|
||||||
|
f:write(python_path)
|
||||||
|
f:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
_nix_python = python_path
|
||||||
|
_nix_discovered = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
---@return boolean success
|
---@return boolean success
|
||||||
function M.setup_python_env()
|
function M.setup_python_env()
|
||||||
if python_env_setup then
|
if python_env_setup then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
local plugin_path = M.get_plugin_path()
|
if _nix_python then
|
||||||
local venv_dir = plugin_path .. '/.venv'
|
logger.log('Python env: nix (python=' .. _nix_python .. ')')
|
||||||
|
python_env_setup = true
|
||||||
if vim.fn.executable('uv') == 0 then
|
return true
|
||||||
logger.log(
|
|
||||||
'uv is not installed. Install it to enable problem scraping: https://docs.astral.sh/uv/',
|
|
||||||
vim.log.levels.WARN
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if vim.fn.isdirectory(venv_dir) == 0 then
|
if vim.fn.executable('uv') == 1 then
|
||||||
logger.log('Setting up Python environment for scrapers...')
|
local plugin_path = M.get_plugin_path()
|
||||||
|
logger.log('Python env: uv sync (dir=' .. plugin_path .. ')')
|
||||||
|
vim.notify('[cp.nvim] Setting up Python environment...', vim.log.levels.INFO)
|
||||||
|
vim.cmd.redraw()
|
||||||
|
|
||||||
local env = vim.fn.environ()
|
local env = vim.fn.environ()
|
||||||
env.VIRTUAL_ENV = ''
|
env.VIRTUAL_ENV = ''
|
||||||
env.PYTHONPATH = ''
|
env.PYTHONPATH = ''
|
||||||
|
|
@ -108,16 +189,35 @@ function M.setup_python_env()
|
||||||
.system({ 'uv', 'sync' }, { cwd = plugin_path, text = true, env = env })
|
.system({ 'uv', 'sync' }, { cwd = plugin_path, text = true, env = env })
|
||||||
:wait()
|
:wait()
|
||||||
if result.code ~= 0 then
|
if result.code ~= 0 then
|
||||||
logger.log('Failed to setup Python environment: ' .. result.stderr, vim.log.levels.ERROR)
|
logger.log(
|
||||||
|
'Failed to setup Python environment: ' .. (result.stderr or ''),
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
logger.log('Python environment setup complete.')
|
if result.stderr and result.stderr ~= '' then
|
||||||
|
logger.log('uv sync stderr: ' .. result.stderr:gsub('%s+$', ''))
|
||||||
end
|
end
|
||||||
|
|
||||||
python_env_setup = true
|
python_env_setup = true
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if vim.fn.executable('nix') == 1 then
|
||||||
|
logger.log('Python env: nix discovery')
|
||||||
|
if discover_nix_python() then
|
||||||
|
python_env_setup = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
'No Python environment available. Install uv (https://docs.astral.sh/uv/) or use nix.',
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
--- Configure the buffer with good defaults
|
--- Configure the buffer with good defaults
|
||||||
---@param filetype? string
|
---@param filetype? string
|
||||||
function M.create_buffer_with_options(filetype)
|
function M.create_buffer_with_options(filetype)
|
||||||
|
|
@ -162,20 +262,12 @@ function M.check_required_runtime()
|
||||||
|
|
||||||
local time = M.time_capability()
|
local time = M.time_capability()
|
||||||
if not time.ok then
|
if not time.ok then
|
||||||
return false, 'GNU time not found: ' .. (time.reason or '')
|
return false, time.reason
|
||||||
end
|
end
|
||||||
|
|
||||||
local timeout = M.timeout_capability()
|
local timeout = M.timeout_capability()
|
||||||
if not timeout.ok then
|
if not timeout.ok then
|
||||||
return false, 'GNU timeout not found: ' .. (timeout.reason or '')
|
return false, timeout.reason
|
||||||
end
|
|
||||||
|
|
||||||
if vim.fn.executable('uv') ~= 1 then
|
|
||||||
return false, 'uv not found (https://docs.astral.sh/uv/)'
|
|
||||||
end
|
|
||||||
|
|
||||||
if not M.setup_python_env() then
|
|
||||||
return false, 'failed to set up Python virtual environment'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
@ -225,7 +317,11 @@ local function find_gnu_timeout()
|
||||||
|
|
||||||
_timeout_cached = true
|
_timeout_cached = true
|
||||||
_timeout_path = nil
|
_timeout_path = nil
|
||||||
|
if uname and uname.sysname == 'Darwin' then
|
||||||
|
_timeout_reason = 'GNU timeout not found (install via: brew install coreutils)'
|
||||||
|
else
|
||||||
_timeout_reason = 'GNU timeout not found'
|
_timeout_reason = 'GNU timeout not found'
|
||||||
|
end
|
||||||
return _timeout_path, _timeout_reason
|
return _timeout_path, _timeout_reason
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
0
new
0
new
|
|
@ -154,3 +154,17 @@ end, {
|
||||||
return {}
|
return {}
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
local function cp_action(action)
|
||||||
|
return function()
|
||||||
|
require('cp').handle_command({ fargs = { action } })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.keymap.set('n', '<Plug>(cp-run)', cp_action('run'), { desc = 'CP run tests' })
|
||||||
|
vim.keymap.set('n', '<Plug>(cp-panel)', cp_action('panel'), { desc = 'CP open panel' })
|
||||||
|
vim.keymap.set('n', '<Plug>(cp-edit)', cp_action('edit'), { desc = 'CP edit test cases' })
|
||||||
|
vim.keymap.set('n', '<Plug>(cp-next)', cp_action('next'), { desc = 'CP next problem' })
|
||||||
|
vim.keymap.set('n', '<Plug>(cp-prev)', cp_action('prev'), { desc = 'CP previous problem' })
|
||||||
|
vim.keymap.set('n', '<Plug>(cp-pick)', cp_action('pick'), { desc = 'CP pick contest' })
|
||||||
|
vim.keymap.set('n', '<Plug>(cp-interact)', cp_action('interact'), { desc = 'CP interactive mode' })
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@ dependencies = [
|
||||||
"ndjson>=0.3.1",
|
"ndjson>=0.3.1",
|
||||||
"pydantic>=2.11.10",
|
"pydantic>=2.11.10",
|
||||||
"requests>=2.32.5",
|
"requests>=2.32.5",
|
||||||
"scrapling[fetchers]>=0.3.5",
|
|
||||||
"types-requests>=2.32.4.20250913",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from scrapling.fetchers import Fetcher
|
from curl_cffi import requests as curl_requests
|
||||||
|
|
||||||
from .base import BaseScraper
|
from .base import BaseScraper
|
||||||
from .models import (
|
from .models import (
|
||||||
|
|
@ -50,8 +50,9 @@ def _extract_memory_limit(html: str) -> float:
|
||||||
|
|
||||||
|
|
||||||
def _fetch_html_sync(url: str) -> str:
|
def _fetch_html_sync(url: str) -> str:
|
||||||
response = Fetcher.get(url)
|
response = curl_requests.get(url, impersonate="chrome", timeout=TIMEOUT_S)
|
||||||
return str(response.body)
|
response.raise_for_status()
|
||||||
|
return response.text
|
||||||
|
|
||||||
|
|
||||||
class CodeChefScraper(BaseScraper):
|
class CodeChefScraper(BaseScraper):
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,12 @@
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from bs4 import BeautifulSoup, Tag
|
from bs4 import BeautifulSoup, Tag
|
||||||
from scrapling.fetchers import Fetcher
|
from curl_cffi import requests as curl_requests
|
||||||
|
|
||||||
from .base import BaseScraper
|
from .base import BaseScraper
|
||||||
from .models import (
|
from .models import (
|
||||||
|
|
@ -19,10 +18,6 @@ from .models import (
|
||||||
TestCase,
|
TestCase,
|
||||||
)
|
)
|
||||||
|
|
||||||
# suppress scrapling logging - https://github.com/D4Vinci/Scrapling/issues/31)
|
|
||||||
logging.getLogger("scrapling").setLevel(logging.CRITICAL)
|
|
||||||
|
|
||||||
|
|
||||||
BASE_URL = "https://codeforces.com"
|
BASE_URL = "https://codeforces.com"
|
||||||
API_CONTEST_LIST_URL = f"{BASE_URL}/api/contest.list"
|
API_CONTEST_LIST_URL = f"{BASE_URL}/api/contest.list"
|
||||||
TIMEOUT_SECONDS = 30
|
TIMEOUT_SECONDS = 30
|
||||||
|
|
@ -83,7 +78,7 @@ def _extract_title(block: Tag) -> tuple[str, str]:
|
||||||
|
|
||||||
def _extract_samples(block: Tag) -> tuple[list[TestCase], bool]:
|
def _extract_samples(block: Tag) -> tuple[list[TestCase], bool]:
|
||||||
st = block.find("div", class_="sample-test")
|
st = block.find("div", class_="sample-test")
|
||||||
if not st:
|
if not isinstance(st, Tag):
|
||||||
return [], False
|
return [], False
|
||||||
|
|
||||||
input_pres: list[Tag] = [
|
input_pres: list[Tag] = [
|
||||||
|
|
@ -140,10 +135,9 @@ def _is_interactive(block: Tag) -> bool:
|
||||||
|
|
||||||
def _fetch_problems_html(contest_id: str) -> str:
|
def _fetch_problems_html(contest_id: str) -> str:
|
||||||
url = f"{BASE_URL}/contest/{contest_id}/problems"
|
url = f"{BASE_URL}/contest/{contest_id}/problems"
|
||||||
page = Fetcher.get(
|
response = curl_requests.get(url, impersonate="chrome", timeout=TIMEOUT_SECONDS)
|
||||||
url,
|
response.raise_for_status()
|
||||||
)
|
return response.text
|
||||||
return page.html_content
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_all_blocks(html: str) -> list[dict[str, Any]]:
|
def _parse_all_blocks(html: str) -> list[dict[str, Any]]:
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from typing import Any
|
||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
from scrapling import fetchers
|
from curl_cffi import requests as curl_requests
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
FIX = Path(__file__).resolve().parent / "fixtures"
|
FIX = Path(__file__).resolve().parent / "fixtures"
|
||||||
|
|
@ -136,12 +136,15 @@ def run_scraper_offline(fixture_text):
|
||||||
|
|
||||||
case "codeforces":
|
case "codeforces":
|
||||||
|
|
||||||
class MockCodeForcesPage:
|
class MockCurlResponse:
|
||||||
def __init__(self, html: str):
|
def __init__(self, html: str):
|
||||||
self.html_content = html
|
self.text = html
|
||||||
|
|
||||||
def _mock_stealthy_fetch(url: str, **kwargs):
|
def raise_for_status(self):
|
||||||
return MockCodeForcesPage(_router_codeforces(url=url))
|
pass
|
||||||
|
|
||||||
|
def _mock_curl_get(url: str, **kwargs):
|
||||||
|
return MockCurlResponse(_router_codeforces(url=url))
|
||||||
|
|
||||||
def _mock_requests_get(url: str, **kwargs):
|
def _mock_requests_get(url: str, **kwargs):
|
||||||
if "api/contest.list" in url:
|
if "api/contest.list" in url:
|
||||||
|
|
@ -172,7 +175,7 @@ def run_scraper_offline(fixture_text):
|
||||||
raise AssertionError(f"Unexpected requests.get call: {url}")
|
raise AssertionError(f"Unexpected requests.get call: {url}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"Fetcher.get": _mock_stealthy_fetch,
|
"curl_requests.get": _mock_curl_get,
|
||||||
"requests.get": _mock_requests_get,
|
"requests.get": _mock_requests_get,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,21 +215,23 @@ def run_scraper_offline(fixture_text):
|
||||||
return MockResponse(data)
|
return MockResponse(data)
|
||||||
raise AssertionError(f"No fixture for CodeChef url={url!r}")
|
raise AssertionError(f"No fixture for CodeChef url={url!r}")
|
||||||
|
|
||||||
class MockCodeChefPage:
|
class MockCodeChefCurlResponse:
|
||||||
def __init__(self, html: str):
|
def __init__(self, html: str):
|
||||||
self.body = html
|
self.text = html
|
||||||
self.status = 200
|
|
||||||
|
|
||||||
def _mock_stealthy_fetch(url: str, **kwargs):
|
def raise_for_status(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _mock_curl_get(url: str, **kwargs):
|
||||||
if "/problems/" in url:
|
if "/problems/" in url:
|
||||||
problem_id = url.rstrip("/").split("/")[-1]
|
problem_id = url.rstrip("/").split("/")[-1]
|
||||||
html = fixture_text(f"codechef/{problem_id}.html")
|
html = fixture_text(f"codechef/{problem_id}.html")
|
||||||
return MockCodeChefPage(html)
|
return MockCodeChefCurlResponse(html)
|
||||||
raise AssertionError(f"No fixture for CodeChef url={url!r}")
|
raise AssertionError(f"No fixture for CodeChef url={url!r}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"__offline_get_async": __offline_get_async,
|
"__offline_get_async": __offline_get_async,
|
||||||
"Fetcher.get": _mock_stealthy_fetch,
|
"curl_requests.get": _mock_curl_get,
|
||||||
}
|
}
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
|
|
@ -245,7 +250,7 @@ def run_scraper_offline(fixture_text):
|
||||||
offline_fetches = _make_offline_fetches(scraper_name)
|
offline_fetches = _make_offline_fetches(scraper_name)
|
||||||
|
|
||||||
if scraper_name == "codeforces":
|
if scraper_name == "codeforces":
|
||||||
fetchers.Fetcher.get = offline_fetches["Fetcher.get"]
|
curl_requests.get = offline_fetches["curl_requests.get"]
|
||||||
requests.get = offline_fetches["requests.get"]
|
requests.get = offline_fetches["requests.get"]
|
||||||
elif scraper_name == "atcoder":
|
elif scraper_name == "atcoder":
|
||||||
ns._fetch = offline_fetches["_fetch"]
|
ns._fetch = offline_fetches["_fetch"]
|
||||||
|
|
@ -254,7 +259,7 @@ def run_scraper_offline(fixture_text):
|
||||||
httpx.AsyncClient.get = offline_fetches["__offline_fetch_text"]
|
httpx.AsyncClient.get = offline_fetches["__offline_fetch_text"]
|
||||||
elif scraper_name == "codechef":
|
elif scraper_name == "codechef":
|
||||||
httpx.AsyncClient.get = offline_fetches["__offline_get_async"]
|
httpx.AsyncClient.get = offline_fetches["__offline_get_async"]
|
||||||
fetchers.Fetcher.get = offline_fetches["Fetcher.get"]
|
curl_requests.get = offline_fetches["curl_requests.get"]
|
||||||
|
|
||||||
scraper_class = getattr(ns, scraper_classes[scraper_name])
|
scraper_class = getattr(ns, scraper_classes[scraper_name])
|
||||||
scraper = scraper_class()
|
scraper = scraper_class()
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,6 @@ from scrapers.models import (
|
||||||
TestsResult,
|
TestsResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
MODEL_FOR_MODE = {
|
|
||||||
"metadata": MetadataResult,
|
|
||||||
"contests": ContestListResult,
|
|
||||||
}
|
|
||||||
|
|
||||||
MATRIX = {
|
MATRIX = {
|
||||||
"cses": {
|
"cses": {
|
||||||
"metadata": ("introductory_problems",),
|
"metadata": ("introductory_problems",),
|
||||||
|
|
@ -43,16 +38,15 @@ def test_scraper_offline_fixture_matrix(run_scraper_offline, scraper, mode):
|
||||||
assert rc in (0, 1), f"Bad exit code {rc}"
|
assert rc in (0, 1), f"Bad exit code {rc}"
|
||||||
assert objs, f"No JSON output for {scraper}:{mode}"
|
assert objs, f"No JSON output for {scraper}:{mode}"
|
||||||
|
|
||||||
if mode in ("metadata", "contests"):
|
|
||||||
Model = MODEL_FOR_MODE[mode]
|
|
||||||
model = Model.model_validate(objs[-1])
|
|
||||||
assert model is not None
|
|
||||||
assert model.success is True
|
|
||||||
if mode == "metadata":
|
if mode == "metadata":
|
||||||
|
model = MetadataResult.model_validate(objs[-1])
|
||||||
|
assert model.success is True
|
||||||
assert model.url
|
assert model.url
|
||||||
assert len(model.problems) >= 1
|
assert len(model.problems) >= 1
|
||||||
assert all(isinstance(p.id, str) and p.id for p in model.problems)
|
assert all(isinstance(p.id, str) and p.id for p in model.problems)
|
||||||
else:
|
elif mode == "contests":
|
||||||
|
model = ContestListResult.model_validate(objs[-1])
|
||||||
|
assert model.success is True
|
||||||
assert len(model.contests) >= 1
|
assert len(model.contests) >= 1
|
||||||
else:
|
else:
|
||||||
assert len(objs) >= 1, "No test objects returned"
|
assert len(objs) >= 1, "No test objects returned"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue