[CI] Improve automated checks (#2702)

* same stuff, but with some more spurious error supression

* fix issue in permissions found in this

* fix a few more spurious errors

* fix another issue

* semi-spurious error fixes

* .

* formatting

* move this to properly log

* distutils import + virtualenv

* more fixes
This commit is contained in:
Michael H 2019-06-02 13:42:58 -04:00 committed by Kowlin
parent 9116cd02e6
commit 16443c8cc0
20 changed files with 205 additions and 43 deletions

148
.pylintrc Normal file
View File

@ -0,0 +1,148 @@
[MASTER]
# Specify a configuration file.
#rcfile=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=pytest
# Pickle collected data for later comparisons.
persistent=no
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# DO NOT CHANGE THIS VALUE # Use multiple processes to speed up Pylint.
jobs=1
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# Allow optimization of some AST trees. This will activate a peephole AST
# optimizer, which will apply various small optimizations. For instance, it can
# be used to obtain the result of joining multiple strings with the addition
# operator. Joining a lot of strings can lead to a maximum recursion error in
# Pylint and this flag can prevent that. It has one side effect, the resulting
# AST will be different than the one from reality.
optimize-ast=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time. See also the "--disable" option for examples.
enable=all
disable=C, # black is enforcing this for us already, incompatibly
W, # unbroaden this to the below specifics later on.
W0107, # uneccessary pass is stylisitc in most places
W0212, # Should likely refactor around protected access warnings later
W1203, # fstrings are too fast to care about enforcing this.
W0612, # unused vars can sometimes indicate an issue, but ...
W1401, # Should probably fix the reason this is disabled (start up screen)
W0511, # Nope, todos are fine for future people to see things to do.
W0613, # Too many places where we need to take unused args do to d.py ... also menus
W0221, # Overriden converters.
W0223, # abstractmethod not defined in mixins is expected
I, # ...
R # While some of these have merit, It's too large a burden to enable this right now.
[REPORTS]
output-format=parseable
files-output=no
reports=no
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# TODO: Write a plyint plugin to allow this with these mixin classes
# To use the abstractmethod we know will be defined in the final class.
ignored-classes=redbot.cogs.mod.movetocore.MoveToCore,
redbot.cogs.mod.kickban.KickBanMixin,
redbot.cogs.mod.mutes.MuteMixin,
redbot.cogs.mod.names.ModInfo,
redbot.cogs.mod.settings.ModSettings,
redbot.cogs.mod.events.Events
ignored-modules=distutils # https://github.com/PyCQA/pylint/issues/73
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_$|dummy
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,__call__
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception,discord.DiscordException

View File

@ -3,7 +3,7 @@ import pathlib
import platform import platform
import shutil import shutil
import asyncio import asyncio
import asyncio.subprocess import asyncio.subprocess # disables for # https://github.com/PyCQA/pylint/issues/1469
import logging import logging
import re import re
import tempfile import tempfile
@ -42,7 +42,7 @@ class ServerManager:
def __init__(self) -> None: def __init__(self) -> None:
self.ready = asyncio.Event() self.ready = asyncio.Event()
self._proc: Optional[asyncio.subprocess.Process] = None self._proc: Optional[asyncio.subprocess.Process] = None # pylint:disable=no-member
self._monitor_task: Optional[asyncio.Task] = None self._monitor_task: Optional[asyncio.Task] = None
self._shutdown: bool = False self._shutdown: bool = False
@ -67,7 +67,7 @@ class ServerManager:
shutil.copyfile(BUNDLED_APP_YML, LAVALINK_APP_YML) shutil.copyfile(BUNDLED_APP_YML, LAVALINK_APP_YML)
args = await self._get_jar_args() args = await self._get_jar_args()
self._proc = await asyncio.subprocess.create_subprocess_exec( self._proc = await asyncio.subprocess.create_subprocess_exec( # pylint:disable=no-member
*args, *args,
cwd=str(LAVALINK_DOWNLOAD_DIR), cwd=str(LAVALINK_DOWNLOAD_DIR),
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
@ -117,7 +117,7 @@ class ServerManager:
""" """
This assumes we've already checked that java exists. This assumes we've already checked that java exists.
""" """
_proc: asyncio.subprocess.Process = await asyncio.create_subprocess_exec( _proc: asyncio.subprocess.Process = await asyncio.create_subprocess_exec( # pylint:disable=no-member
"java", "-version", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE "java", "-version", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
) )
# java -version outputs to stderr # java -version outputs to stderr
@ -173,7 +173,7 @@ class ServerManager:
await self.start() await self.start()
else: else:
log.critical( log.critical(
"Your Java is borked. Please find the hs_err_pid{}.log file" "Your Java is borked. Please find the hs_err_pid%d.log file"
" in the Audio data folder and report this issue.", " in the Audio data folder and report this issue.",
self._proc.pid, self._proc.pid,
) )
@ -222,7 +222,7 @@ class ServerManager:
return True return True
args = await cls._get_jar_args() args = await cls._get_jar_args()
args.append("--version") args.append("--version")
_proc = await asyncio.subprocess.create_subprocess_exec( _proc = await asyncio.subprocess.create_subprocess_exec( # pylint:disable=no-member
*args, *args,
cwd=str(LAVALINK_DOWNLOAD_DIR), cwd=str(LAVALINK_DOWNLOAD_DIR),
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,

View File

@ -1,3 +1,4 @@
import logging
import re import re
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Union, List, Callable, Set from typing import Union, List, Callable, Set
@ -8,12 +9,13 @@ from redbot.core import checks, commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.mod import slow_deletion, mass_purge from redbot.core.utils.mod import slow_deletion, mass_purge
from redbot.cogs.mod.log import log
from redbot.core.utils.predicates import MessagePredicate from redbot.core.utils.predicates import MessagePredicate
from .converters import RawMessageIds from .converters import RawMessageIds
_ = Translator("Cleanup", __file__) _ = Translator("Cleanup", __file__)
log = logging.getLogger("red.cleanup")
@cog_i18n(_) @cog_i18n(_)
class Cleanup(commands.Cog): class Cleanup(commands.Cog):
@ -302,13 +304,13 @@ class Cleanup(commands.Cog):
author = ctx.author author = ctx.author
try: try:
mone = await channel.fetch_message(one) mone = await channel.fetch_message(one)
except discord.errors.Notfound: except discord.errors.NotFound:
return await ctx.send( return await ctx.send(
_("Could not find a message with the ID of {id}.".format(id=one)) _("Could not find a message with the ID of {id}.".format(id=one))
) )
try: try:
mtwo = await channel.fetch_message(two) mtwo = await channel.fetch_message(two)
except discord.errors.Notfound: except discord.errors.NotFound:
return await ctx.send( return await ctx.send(
_("Could not find a message with the ID of {id}.".format(id=two)) _("Could not find a message with the ID of {id}.".format(id=two))
) )

View File

@ -467,7 +467,9 @@ class Economy(commands.Cog):
sign = " " sign = " "
if i == 1: if i == 1:
sign = ">" sign = ">"
slot += "{}{} {} {}\n".format(sign, *[c.value for c in row]) slot += "{}{} {} {}\n".format(
sign, *[c.value for c in row] # pylint: disable=no-member
)
payout = PAYOUTS.get(rows[1]) payout = PAYOUTS.get(rows[1])
if not payout: if not payout:

View File

@ -1,13 +1,14 @@
import logging
from datetime import datetime from datetime import datetime
from collections import defaultdict, deque from collections import defaultdict, deque
import discord import discord
from redbot.core import i18n, modlog, commands from redbot.core import i18n, modlog, commands
from redbot.core.utils.mod import is_mod_or_superior from redbot.core.utils.mod import is_mod_or_superior
from . import log
from .abc import MixinMeta from .abc import MixinMeta
_ = i18n.Translator("Mod", __file__) _ = i18n.Translator("Mod", __file__)
log = logging.getLogger("red.mod")
class Events(MixinMeta): class Events(MixinMeta):

View File

@ -1,5 +1,6 @@
import asyncio import asyncio
import contextlib import contextlib
import logging
from collections import namedtuple from collections import namedtuple
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import cast, Optional, Union from typing import cast, Optional, Union
@ -10,8 +11,8 @@ from redbot.core.utils.chat_formatting import pagify
from redbot.core.utils.mod import is_allowed_by_hierarchy, get_audit_reason from redbot.core.utils.mod import is_allowed_by_hierarchy, get_audit_reason
from .abc import MixinMeta from .abc import MixinMeta
from .converters import RawUserIds from .converters import RawUserIds
from .log import log
log = logging.getLogger("red.mod")
_ = i18n.Translator("Mod", __file__) _ = i18n.Translator("Mod", __file__)

View File

@ -1,4 +0,0 @@
import logging
log = logging.getLogger("red.mod")

View File

@ -1,3 +1,4 @@
import logging
import asyncio import asyncio
import contextlib import contextlib
@ -5,8 +6,8 @@ import discord
from redbot.core import commands, checks, i18n from redbot.core import commands, checks, i18n
from redbot.core.utils.chat_formatting import box from redbot.core.utils.chat_formatting import box
from .abc import MixinMeta from .abc import MixinMeta
from .log import log
log = logging.getLogger("red.mod")
_ = i18n.Translator("Mod", __file__) _ = i18n.Translator("Mod", __file__)

View File

@ -28,6 +28,9 @@ COG = "COG"
COMMAND = "COMMAND" COMMAND = "COMMAND"
GLOBAL = 0 GLOBAL = 0
_OldConfigSchema = Dict[int, Dict[str, Dict[str, Dict[str, Dict[str, List[int]]]]]]
_NewConfigSchema = Dict[str, Dict[int, Dict[str, Dict[int, bool]]]]
# The strings in the schema are constants and should get extracted, but not translated until # The strings in the schema are constants and should get extracted, but not translated until
# runtime. # runtime.
translate = _ translate = _
@ -625,9 +628,6 @@ class Permissions(commands.Cog):
await self.config.custom(COMMAND).set(new_cmd_rules) await self.config.custom(COMMAND).set(new_cmd_rules)
await self.config.version.set(__version__) await self.config.version.set(__version__)
_OldConfigSchema = Dict[int, Dict[str, Dict[str, Dict[str, Dict[str, List[int]]]]]]
_NewConfigSchema = Dict[str, Dict[int, Dict[str, Dict[int, bool]]]]
@staticmethod @staticmethod
def _get_updated_schema( def _get_updated_schema(
old_config: _OldConfigSchema old_config: _OldConfigSchema

View File

@ -215,7 +215,7 @@ class TwitchStream(Stream):
status = "Untitled broadcast" status = "Untitled broadcast"
if is_rerun: if is_rerun:
status += " - Rerun" status += " - Rerun"
embed = discord.Embed(title=status, url=url) embed = discord.Embed(title=status, url=url, color=0x6441A4)
embed.set_author(name=channel["display_name"]) embed.set_author(name=channel["display_name"])
embed.add_field(name="Followers", value=channel["followers"]) embed.add_field(name="Followers", value=channel["followers"])
embed.add_field(name="Total views", value=channel["views"]) embed.add_field(name="Total views", value=channel["views"])
@ -224,7 +224,6 @@ class TwitchStream(Stream):
embed.set_image(url=rnd(data["stream"]["preview"]["medium"])) embed.set_image(url=rnd(data["stream"]["preview"]["medium"]))
if channel["game"]: if channel["game"]:
embed.set_footer(text="Playing: " + channel["game"]) embed.set_footer(text="Playing: " + channel["game"])
embed.color = 0x6441A4
return embed return embed
@ -260,14 +259,13 @@ class HitboxStream(Stream):
livestream = data["livestream"][0] livestream = data["livestream"][0]
channel = livestream["channel"] channel = livestream["channel"]
url = channel["channel_link"] url = channel["channel_link"]
embed = discord.Embed(title=livestream["media_status"], url=url) embed = discord.Embed(title=livestream["media_status"], url=url, color=0x98CB00)
embed.set_author(name=livestream["media_name"]) embed.set_author(name=livestream["media_name"])
embed.add_field(name="Followers", value=channel["followers"]) embed.add_field(name="Followers", value=channel["followers"])
embed.set_thumbnail(url=base_url + channel["user_logo"]) embed.set_thumbnail(url=base_url + channel["user_logo"])
if livestream["media_thumbnail"]: if livestream["media_thumbnail"]:
embed.set_image(url=rnd(base_url + livestream["media_thumbnail"])) embed.set_image(url=rnd(base_url + livestream["media_thumbnail"]))
embed.set_footer(text="Playing: " + livestream["category_name"]) embed.set_footer(text="Playing: " + livestream["category_name"])
embed.color = 0x98CB00
return embed return embed
@ -310,7 +308,7 @@ class MixerStream(Stream):
embed.set_thumbnail(url=default_avatar) embed.set_thumbnail(url=default_avatar)
if data["thumbnail"]: if data["thumbnail"]:
embed.set_image(url=rnd(data["thumbnail"]["url"])) embed.set_image(url=rnd(data["thumbnail"]["url"]))
embed.color = 0x4C90F3 embed.color = 0x4C90F3 # pylint: disable=assigning-non-slot
if data["type"] is not None: if data["type"] is not None:
embed.set_footer(text="Playing: " + data["type"]["name"]) embed.set_footer(text="Playing: " + data["type"]["name"])
return embed return embed
@ -345,13 +343,12 @@ class PicartoStream(Stream):
) )
url = "https://picarto.tv/" + data["name"] url = "https://picarto.tv/" + data["name"]
thumbnail = data["thumbnails"]["web"] thumbnail = data["thumbnails"]["web"]
embed = discord.Embed(title=data["title"], url=url) embed = discord.Embed(title=data["title"], url=url, color=0x4C90F3)
embed.set_author(name=data["name"]) embed.set_author(name=data["name"])
embed.set_image(url=rnd(thumbnail)) embed.set_image(url=rnd(thumbnail))
embed.add_field(name="Followers", value=data["followers"]) embed.add_field(name="Followers", value=data["followers"])
embed.add_field(name="Total views", value=data["viewers_total"]) embed.add_field(name="Total views", value=data["viewers_total"])
embed.set_thumbnail(url=avatar) embed.set_thumbnail(url=avatar)
embed.color = 0x132332
data["tags"] = ", ".join(data["tags"]) data["tags"] = ", ".join(data["tags"])
if not data["tags"]: if not data["tags"]:
@ -362,6 +359,5 @@ class PicartoStream(Stream):
else: else:
data["adult"] = "" data["adult"] = ""
embed.color = 0x4C90F3
embed.set_footer(text="{adult}Category: {category} | Tags: {tags}".format(**data)) embed.set_footer(text="{adult}Category: {category} | Tags: {tags}".format(**data))
return embed return embed

View File

@ -27,7 +27,8 @@ def _is_submodule(parent, child):
return parent == child or child.startswith(parent + ".") return parent == child or child.startswith(parent + ".")
class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # barely spurious warning caused by our intentional shadowing
class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: disable=no-member
"""Mixin for the main bot class. """Mixin for the main bot class.
This exists because `Red` inherits from `discord.AutoShardedClient`, which This exists because `Red` inherits from `discord.AutoShardedClient`, which

View File

@ -704,29 +704,35 @@ def mod():
class _IntKeyDict(Dict[int, _T]): class _IntKeyDict(Dict[int, _T]):
"""Dict subclass which throws KeyError when a non-int key is used.""" """Dict subclass which throws KeyError when a non-int key is used."""
get: Callable
setdefault: Callable
def __getitem__(self, key: Any) -> _T: def __getitem__(self, key: Any) -> _T:
if not isinstance(key, int): if not isinstance(key, int):
raise TypeError("Keys must be of type `int`") raise TypeError("Keys must be of type `int`")
return super().__getitem__(key) return super().__getitem__(key) # pylint: disable=no-member
def __setitem__(self, key: Any, value: _T) -> None: def __setitem__(self, key: Any, value: _T) -> None:
if not isinstance(key, int): if not isinstance(key, int):
raise TypeError("Keys must be of type `int`") raise TypeError("Keys must be of type `int`")
return super().__setitem__(key, value) return super().__setitem__(key, value) # pylint: disable=no-member
class _RulesDict(Dict[Union[int, str], PermState]): class _RulesDict(Dict[Union[int, str], PermState]):
"""Dict subclass which throws a KeyError when an invalid key is used.""" """Dict subclass which throws a KeyError when an invalid key is used."""
get: Callable
setdefault: Callable
def __getitem__(self, key: Any) -> PermState: def __getitem__(self, key: Any) -> PermState:
if key != Requires.DEFAULT and not isinstance(key, int): if key != Requires.DEFAULT and not isinstance(key, int):
raise TypeError(f'Expected "{Requires.DEFAULT}" or int key, not "{key}"') raise TypeError(f'Expected "{Requires.DEFAULT}" or int key, not "{key}"')
return super().__getitem__(key) return super().__getitem__(key) # pylint: disable=no-member
def __setitem__(self, key: Any, value: PermState) -> None: def __setitem__(self, key: Any, value: PermState) -> None:
if key != Requires.DEFAULT and not isinstance(key, int): if key != Requires.DEFAULT and not isinstance(key, int):
raise TypeError(f'Expected "{Requires.DEFAULT}" or int key, not "{key}"') raise TypeError(f'Expected "{Requires.DEFAULT}" or int key, not "{key}"')
return super().__setitem__(key, value) return super().__setitem__(key, value) # pylint: disable=no-member
def _validate_perms_dict(perms: Dict[str, bool]) -> None: def _validate_perms_dict(perms: Dict[str, bool]) -> None:

View File

@ -30,7 +30,7 @@ def get_latest_confs() -> Tuple["Config"]:
return tuple(ret) return tuple(ret)
class _ValueCtxManager(Awaitable[_T], AsyncContextManager[_T]): class _ValueCtxManager(Awaitable[_T], AsyncContextManager[_T]): # pylint: disable=duplicate-bases
"""Context manager implementation of config values. """Context manager implementation of config values.
This class allows mutable config values to be both "get" and "set" from This class allows mutable config values to be both "get" and "set" from
@ -1135,7 +1135,8 @@ class Config:
) )
group = Group(identifier_data, defaults={}, driver=self.driver) group = Group(identifier_data, defaults={}, driver=self.driver)
else: else:
group = self._get_base_group(*scopes) cat, *scopes = scopes
group = self._get_base_group(cat, *scopes)
await group.clear() await group.clear()
async def clear_all(self): async def clear_all(self):

View File

@ -963,13 +963,13 @@ class Core(commands.Cog, CoreLogic):
"message", check=MessagePredicate.same_context(ctx), timeout=60 "message", check=MessagePredicate.same_context(ctx), timeout=60
) )
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.owner.reset_cooldown(ctx) ctx.command.reset_cooldown(ctx)
await ctx.send( await ctx.send(
_("The `{prefix}set owner` request has timed out.").format(prefix=ctx.prefix) _("The `{prefix}set owner` request has timed out.").format(prefix=ctx.prefix)
) )
else: else:
if message.content.strip() == token: if message.content.strip() == token:
self.owner.reset_cooldown(ctx) ctx.command.reset_cooldown(ctx)
await ctx.bot.db.owner.set(ctx.author.id) await ctx.bot.db.owner.set(ctx.author.id)
ctx.bot.owner_id = ctx.author.id ctx.bot.owner_id = ctx.author.id
await ctx.send(_("You have been set as owner.")) await ctx.send(_("You have been set as owner."))

View File

@ -35,7 +35,7 @@ basic_config_default = {"DATA_PATH": None, "COG_PATH_APPEND": "cogs", "CORE_PATH
config_dir = None config_dir = None
appdir = appdirs.AppDirs("Red-DiscordBot") appdir = appdirs.AppDirs("Red-DiscordBot")
if sys.platform == "linux": if sys.platform == "linux":
if 0 < os.getuid() < 1000: if 0 < os.getuid() < 1000: # pylint: disable=no-member
config_dir = Path(appdir.site_data_dir) config_dir = Path(appdir.site_data_dir)
if not config_dir: if not config_dir:
config_dir = Path(appdir.user_config_dir) config_dir = Path(appdir.user_config_dir)

View File

@ -57,7 +57,7 @@ class JsonIO:
tmp_path.replace(self.path) tmp_path.replace(self.path)
# pylint: disable=E1101 # pylint: disable=no-member
try: try:
fd = os.open(self.path.parent, os.O_DIRECTORY) fd = os.open(self.path.parent, os.O_DIRECTORY)
os.fsync(fd) os.fsync(fd)

View File

@ -69,7 +69,8 @@ def safe_delete(pth: Path):
shutil.rmtree(str(pth), ignore_errors=True) shutil.rmtree(str(pth), ignore_errors=True)
class AsyncFilter(AsyncIterator[_T], Awaitable[List[_T]]): # https://github.com/PyCQA/pylint/issues/2717
class AsyncFilter(AsyncIterator[_T], Awaitable[List[_T]]): # pylint: disable=duplicate-bases
"""Class returned by `async_filter`. See that function for details. """Class returned by `async_filter`. See that function for details.
We don't recommend instantiating this class directly. We don't recommend instantiating this class directly.
@ -112,6 +113,9 @@ class AsyncFilter(AsyncIterator[_T], Awaitable[List[_T]]):
async def __flatten(self) -> List[_T]: async def __flatten(self) -> List[_T]:
return [item async for item in self] return [item async for item in self]
def __aiter__(self):
return self
def __await__(self): def __await__(self):
# Simply return the generator filled into a list # Simply return the generator filled into a list
return self.__flatten().__await__() return self.__flatten().__await__()

View File

@ -34,7 +34,7 @@ conversion_log = logging.getLogger("red.converter")
config_dir = None config_dir = None
appdir = appdirs.AppDirs("Red-DiscordBot") appdir = appdirs.AppDirs("Red-DiscordBot")
if sys.platform == "linux": if sys.platform == "linux":
if 0 < os.getuid() < 1000: if 0 < os.getuid() < 1000: # pylint: disable=no-member # Non-exist on win
config_dir = Path(appdir.site_data_dir) config_dir = Path(appdir.site_data_dir)
if not config_dir: if not config_dir:
config_dir = Path(appdir.user_config_dir) config_dir = Path(appdir.user_config_dir)
@ -524,7 +524,7 @@ def convert(instance, backend):
if __name__ == "__main__": if __name__ == "__main__":
try: try:
cli() cli() # pylint: disable=no-value-for-parameter # click
except KeyboardInterrupt: except KeyboardInterrupt:
print("Exiting...") print("Exiting...")
else: else:

View File

@ -84,6 +84,7 @@ test =
pytest==4.2.0 pytest==4.2.0
pytest-asyncio==0.10.0 pytest-asyncio==0.10.0
six==1.12.0 six==1.12.0
pylint==2.3.1
[options.entry_points] [options.entry_points]
console_scripts = console_scripts =

View File

@ -11,13 +11,15 @@ envlist =
skip_missing_interpreters = True skip_missing_interpreters = True
[testenv] [testenv]
description = Run unit tests with pytest description = Run tests and basic automatic issue checking.
whitelist_externals = whitelist_externals =
pytest pytest
pylint
extras = voice, test, mongo extras = voice, test, mongo
commands = commands =
python -m compileall ./redbot/cogs python -m compileall ./redbot/cogs
pytest pytest
pylint ./redbot
[testenv:docs] [testenv:docs]
description = Attempt to build docs with sphinx-build description = Attempt to build docs with sphinx-build