• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

AntiCompositeNumber / iNaturalistReviewer / 12455756921

22 Dec 2024 04:07PM UTC coverage: 62.943% (-0.7%) from 63.692%
12455756921

push

github

AntiCompositeNumber
Check untagged error log directly instead of skip list

191 of 346 branches covered (55.2%)

Branch coverage included in aggregate %.

7 of 9 new or added lines in 2 files covered. (77.78%)

66 existing lines in 1 file now uncovered.

643 of 979 relevant lines covered (65.68%)

0.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/inrcli.py
1
#!/usr/bin/env python3
2
# coding: utf-8
3
# SPDX-License-Identifier: GPL-3.0-or-later
4
# Copyright 2023 AntiCompositeNumber
5

6
import click
×
7
import pywikibot  # type: ignore
×
8
import pywikibot.bot  # type: ignore
×
9
import logging
×
10
import os
×
11
import sys
×
12
import difflib
×
13
import webbrowser
×
14
from typing import Sequence, Dict, Optional, Tuple
×
15

16
os.environ["LOG_FILE"] = "stderr"
×
17
os.environ["LOG_LEVEL"] = "WARNING"
×
18

19
import inrbot  # noqa: E402
×
20

21
inrbot.run_override = True
×
22
inrbot.summary_tag = f"(inrcli {inrbot.__version__})"
×
23
site = inrbot.site
×
24
logger = logging.getLogger("manual")
×
25
ids: Dict[pywikibot.Page, Optional[inrbot.iNaturalistID]] = {}
×
26
auto_open = False
×
UNCOV
27
last_ina_id = None
×
28

UNCOV
29
inrbot.config.update(
×
30
    {
31
        "fail_tag": "{{copyvio|License review not passed: "
32
        "iNaturalist author is using $review_license: $source_url}}\n",
33
        "fail_warn": "\n\n{{subst:Copyvionote |1=$filename "
34
        "|2=License review "
35
        "not passed: iNaturalist author is using $review_license: $source_url }} ~~~~",
36
        "review_summary": "Semi-automatic license review: "
37
        "$status $review_license $tag",
38
        "old_fail_warn": "\n\n{{subst:image permission|1=$filename}} "
39
        "License review not passed: iNaturalist author is "
40
        "using $review_license: $source_url. ~~~~",
41
        "use_wayback": False,
42
    }
43
)
44

45

46
class SkipFile(Exception):
×
UNCOV
47
    pass
×
48

49

UNCOV
50
def manual_compare(
×
51
    com_img: inrbot.CommonsImage, ina_img: inrbot.iNaturalistImage, **kwargs
52
):
53
    if ids.get(com_img.page, None) == ina_img.id and ina_img.id.type == "photos":
×
UNCOV
54
        return True
×
55
    else:
UNCOV
56
        return ask_compare(com_img, ina_img)
×
57

58

59
def ask_compare(com_img: inrbot.CommonsImage, ina_img: inrbot.iNaturalistImage):
×
60
    if click.confirm(f"Show {ina_img.id}?", default=False):
×
61
        com_img.image.show(title=com_img.page.title())
×
62
        ina_img.image.show(title=str(ina_img.id))
×
63
    res = click.confirm("Do these images match?", default=False)
×
UNCOV
64
    return res
×
65

66

UNCOV
67
inrbot.compare_methods.insert(0, ("manual", manual_compare))
×
68

69

70
class ManualCommonsPage(inrbot.CommonsPage):
×
71
    def __init__(self, *args, **kwargs):
×
UNCOV
72
        super().__init__(*args, **kwargs)
×
73

UNCOV
74
    def check_can_run(self) -> bool:
×
75
        """Determinies if the bot should run on this page and returns a bool."""
76
        page = self.page
×
NEW
77
        if (not page.has_permission("edit")) or (not page.botMayEdit()):
×
UNCOV
78
            return False
×
79
        else:
UNCOV
80
            return True
×
81

82
    def get_old_archive_(self):
×
UNCOV
83
        return super().get_old_archive()
×
84

UNCOV
85
    def get_old_archive(self):
×
86
        # Archives will have already been reviewed by the archive_status_hook
87
        if self.status != "fail":
×
88
            return super().get_old_archive()
×
UNCOV
89
        return ""
×
90

91
    def archive_status_hook(self) -> None:
×
92
        if self._status == "fail":
×
93
            super().get_old_archive()
×
94
            if self.archive:
×
UNCOV
95
                print(
×
96
                    f"This file would fail because of the {self.ina_license} license, "
97
                    f"but an archived copy is available at {self.archive}."
98
                )
UNCOV
99
                new_license = click.prompt(
×
100
                    "Archive license (leave blank for no change)",
101
                    default=self.ina_license,
102
                )
103
                if new_license:
×
104
                    self.ina_license = new_license
×
105
                    del self.status
×
UNCOV
106
                    self.status
×
107

108
    @staticmethod
×
UNCOV
109
    def prompt_photo_url(
×
110
        default: Optional[str] = None,
111
    ) -> Tuple[Optional[inrbot.iNaturalistID], str]:
112
        global last_ina_id
113
        while True:
×
114
            # If default is not None, click will repeatedly prompt until it gets
115
            # an answer. That, however, doesn't mean we got an answer we like.
UNCOV
116
            url = click.prompt("iNaturalist Photos URL", default=default)
×
UNCOV
117
            ina_id = inrbot.parse_ina_url(url)
×
118
            if ina_id == last_ina_id:
×
119
                if click.confirm(
×
120
                    "That's the same URL you gave last time. Are you sure?"
121
                ):
UNCOV
122
                    break
×
123
                else:
UNCOV
124
                    continue
×
125
            break
×
UNCOV
126
        if ina_id is not None:
×
UNCOV
127
            last_ina_id = ina_id
×
UNCOV
128
            return ina_id, ""
×
UNCOV
129
        return ina_id, url
×
130

131
    def pre_save(self, new_text, summary, **kwargs):
×
132
        print(
×
133
            f"{self.page.title(as_link=True)} reviewed with status {self.status} "
134
            f"and license {self.ina_license}"
135
        )
UNCOV
136
        if self.status == "error":
×
137
            raise RuntimeError
×
138

UNCOV
139
        diff = difflib.unified_diff(
×
140
            self.page.get().split("\n"), new_text.split("\n"), lineterm=""
141
        )
142
        print("\n".join(diff))
×
143
        choice = click.confirm("Save the page?", default=True)
×
144
        if choice:
×
145
            return new_text, summary
×
146
        else:
147
            raise RuntimeError
×
148

149
    def id_hook(
×
150
        self,
151
        observations: Sequence[inrbot.iNaturalistID] = [],
152
        photos: Sequence[inrbot.iNaturalistID] = [],
153
        **kwargs,
154
    ):
UNCOV
155
        try:
×
UNCOV
156
            return ids[self.page]
×
UNCOV
157
        except KeyError:
×
UNCOV
158
            return self.ask_url(observations=observations, photos=photos)
×
159
        return None
160

161
    def ask_url(
×
162
        self,
163
        observations: Sequence[inrbot.iNaturalistID] = [],
164
        photos: Sequence[inrbot.iNaturalistID] = [],
165
    ):
166
        print(f"Commons page: {self.page.full_url()}")
×
167
        if observations:
×
168
            print(f"Observation ID found: {str(observations[0])}")
×
169
        if photos:
×
170
            print(f"Photo ID found: {str(photos[0])}")
×
171
        if auto_open:
×
UNCOV
172
            webbrowser.open(self.page.full_url())
×
173
            if observations and not photos:
×
174
                webbrowser.open(str(observations[0]))
×
175
            elif photos:
×
176
                webbrowser.open(str(photos[0]))
×
177

178
        res = click.prompt(
×
179
            "Is this ID correct? [Y/n/r/s/q]",
180
            default="Y",
181
            type=click.Choice("ynrsq", case_sensitive=False),
182
            show_default=False,
183
            show_choices=False,
184
        ).lower()
UNCOV
185
        if res == "y":
×
186
            correct_id = True
×
187
        elif res == "n":
×
188
            correct_id = False
×
UNCOV
189
        elif res == "r":
×
190
            self.remove_untagged_log()
×
UNCOV
191
            raise SkipFile
×
192
        elif res == "s":
×
UNCOV
193
            raise SkipFile
×
UNCOV
194
        elif res == "q":
×
195
            sys.exit()
×
196

197
        if not correct_id:
×
UNCOV
198
            ina_id, url = self.prompt_photo_url()
×
UNCOV
199
            if observations:
×
200
                self.page.text = self.page.text.replace(str(observations[0]), url)
×
201
            if photos:
×
202
                self.page.text = self.page.text.replace(str(photos[0]), url)
×
203
        elif observations and not photos:
×
204
            ina_id, url = self.prompt_photo_url(default="")
×
205
        elif photos:
×
206
            ina_id = photos[0]
×
207
        else:
UNCOV
208
            ina_id = None
×
209
        ids[self.page] = ina_id
×
210
        return ina_id
×
211

UNCOV
212
    def log_untagged_error(self) -> None:
×
213
        # Errors while running in CLI do not need to be logged on-wiki
214
        return
×
215

216

217
inrbot.id_hooks.append(ManualCommonsPage.id_hook)
×
218
inrbot.lock_hooks.append(ManualCommonsPage.archive_status_hook)
×
219
inrbot.pre_save_hooks.append(ManualCommonsPage.pre_save)
×
220

221

222
@click.command()
×
223
@click.argument("target")
×
224
@click.option("--url")
×
225
@click.option("--simulate/--no-simulate")
×
226
@click.option("--reverse", is_flag=True, default=False)
×
227
@click.option("-o", "--auto-open", "auto_open_", is_flag=True, default=False)
×
228
def main(target, url="", simulate=False, reverse=False, auto_open_=False):
×
UNCOV
229
    inrbot.simulate = simulate
×
230
    global auto_open
UNCOV
231
    auto_open = auto_open_
×
UNCOV
232
    if target == "auto":
×
233
        cat = pywikibot.Category(
×
234
            site, "Category:iNaturalist images needing human review"
235
        )
236
        dtt = pywikibot.Page(site, "Template:Deletion template tag")
×
237
        for page in cat.articles(namespaces=6, reverse=reverse):
×
238
            if dtt in set(page.itertemplates()):
×
239
                continue
×
240
            try:
×
241
                mcp = ManualCommonsPage(pywikibot.FilePage(page))
×
242
                mcp.review_file()
×
243
            except SkipFile:
×
244
                continue
×
UNCOV
245
    elif target == "errors":
×
246
        log_page = pywikibot.Page(site, inrbot.config["untagged_log_page"])
×
247
        dtt = pywikibot.Page(site, "Template:Deletion template tag")
×
UNCOV
248
        for page in log_page.linkedPages(namespaces=6, follow_redirects=True):
×
249
            mcp = ManualCommonsPage(pywikibot.FilePage(page))
×
UNCOV
250
            if (
×
251
                not page.exists()
252
                or dtt in set(page.itertemplates())
253
                or mcp.check_has_template()
254
            ):
255
                mcp.remove_untagged_log()
×
UNCOV
256
                continue
×
UNCOV
257
            try:
×
UNCOV
258
                mcp.review_file()
×
UNCOV
259
            except SkipFile:
×
UNCOV
260
                continue
×
UNCOV
261
    elif target == "ask":
×
UNCOV
262
        while True:
×
UNCOV
263
            new_target = click.prompt("Target", default="")
×
UNCOV
264
            if not new_target:
×
UNCOV
265
                break
×
UNCOV
266
            ManualCommonsPage(pywikibot.FilePage(site, new_target)).review_file()
×
267
    else:
UNCOV
268
        page = pywikibot.FilePage(site, target)
×
UNCOV
269
        if url:
×
270
            # TODO: Add validation
UNCOV
271
            ids[page] = inrbot.parse_ina_url(url)
×
272

UNCOV
273
        ManualCommonsPage(page).review_file()
×
274

275

UNCOV
276
if __name__ == "__main__":
×
UNCOV
277
    main()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc