#!/usr/bin/python
"""Script to create user-config.py."""
#
# (C) Pywikibot team, 2010-2021
#
# Distributed under the terms of the MIT license.
#
import codecs
import os
import re
import sys
from collections import namedtuple
from textwrap import fill
from typing import Optional
from generate_family_file import _import_with_no_user_config
if sys.version_info[:2] >= (3, 9):
Tuple = tuple
else:
from typing import Tuple
# DISABLED_SECTIONS cannot be copied; variables must be set manually
DISABLED_SECTIONS = {
'USER INTERFACE SETTINGS', # uses sys
'EXTERNAL EDITOR SETTINGS', # uses os
}
OBSOLETE_SECTIONS = {
'ACCOUNT SETTINGS', # already set
}
# Disable user-config usage as we are creating it here
pywikibot = _import_with_no_user_config('pywikibot')
config, __url__ = pywikibot.config, pywikibot.__url__
base_dir = pywikibot.config.base_dir
try:
console_encoding = sys.stdout.encoding
# unittests fails with "StringIO instance has no attribute 'encoding'"
except AttributeError:
console_encoding = None
# the directory in which generate_user_files.py is located
pywikibot_dir = sys.path[0]
if console_encoding is None or sys.platform == 'cygwin':
console_encoding = 'iso-8859-1'
USER_BASENAME = 'user-config.py'
PASS_BASENAME = 'user-password.py'
[docs]def change_base_dir():
"""Create a new user directory."""
while True:
new_base = pywikibot.input('New user directory? ')
new_base = os.path.abspath(new_base)
if os.path.exists(new_base):
if os.path.isfile(new_base):
pywikibot.error('there is an existing file with that name.')
continue
# make sure user can read and write this directory
if not os.access(new_base, os.R_OK | os.W_OK):
pywikibot.error('directory access restricted')
continue
pywikibot.output('Using existing directory')
else:
try:
os.mkdir(new_base, pywikibot.config.private_files_permission)
except Exception as e:
pywikibot.error('directory creation failed: {}'.format(e))
continue
pywikibot.output('Created new directory.')
break
if new_base == pywikibot.config.get_base_dir(new_base):
# config would find that file
return new_base
msg = fill("""WARNING: Your user files will be created in the directory
'{new_base}' you have chosen. To access these files, you will either have
to use the argument "-dir:{new_base}" every time you run the bot, or set
the environment variable "PYWIKIBOT_DIR" equal to this directory name in
your operating system. See your operating system documentation for how to
set environment variables.""".format(new_base=new_base), width=76)
pywikibot.output(msg)
if pywikibot.input_yn('Is this OK?', default=False, automatic_quit=False):
return new_base
pywikibot.output('Aborting changes.')
return False
[docs]def file_exists(filename):
"""Return whether the file exists and print a message if it exists."""
if os.path.exists(filename):
pywikibot.output('{1} already exists in the target directory "{0}".'
.format(*os.path.split(filename)))
return True
return False
[docs]def get_site_and_lang(default_family: Optional[str] = 'wikipedia',
default_lang: Optional[str] = 'en',
default_username: Optional[str] = None, force=False):
"""
Ask the user for the family, language and username.
:param default_family: The default family which should be chosen.
:param default_lang: The default language which should be chosen, if the
family supports this language.
:param default_username: The default username which should be chosen.
:return: The family, language and username
:rtype: tuple of three str
"""
known_families = sorted(pywikibot.config.family_files.keys())
if default_family not in known_families:
default_family = None
fam = pywikibot.bot.input_list_choice(
'Select family of sites we are working on, '
'just enter the number or name',
known_families,
force=force,
default=default_family)
fam = pywikibot.family.Family.load(fam)
if hasattr(fam, 'langs'):
if hasattr(fam, 'languages_by_size'):
by_size = [code for code in fam.languages_by_size
if code in fam.langs.keys()]
else:
by_size = []
known_langs = by_size + sorted(
set(fam.langs.keys()).difference(by_size))
else:
known_langs = []
if not known_langs:
pywikibot.output('There were no known languages found in {}.'
.format(fam.name))
default_lang = None
elif len(known_langs) == 1:
pywikibot.output('The only known language: {}'.format(known_langs[0]))
default_lang = known_langs[0]
else:
pywikibot.output('This is the list of known languages:')
pywikibot.output(', '.join(known_langs))
if default_lang not in known_langs:
if default_lang != 'en' and 'en' in known_langs:
default_lang = 'en'
else:
default_lang = None
message = "The language code of the site we're working on"
mycode = None
while not mycode:
mycode = pywikibot.input(message, default=default_lang, force=force)
if known_langs and mycode and mycode not in known_langs:
if not pywikibot.input_yn(
fill('The language code {} is not in the list of known '
'languages. Do you want to continue?'.format(mycode)),
default=False, automatic_quit=False):
mycode = None
message = 'Username on {}:{}'.format(mycode, fam.name)
username = pywikibot.input(message, default=default_username, force=force)
# Escape ''s
if username:
username = username.replace("'", "\\'")
return fam.name, mycode, username
EXTENDED_CONFIG = """\
# This is an automatically generated file. You can find more
# configuration parameters in 'config.py' file or refer
# https://doc.wikimedia.org/pywikibot/master/api_ref/pywikibot.config.html
# The family of sites to be working on.
# Pywikibot will import families/xxx_family.py so if you want to change
# this variable, you have to ensure that such a file exists. You may use
# generate_family_file to create one.
family = '{main_family}'
# The language code of the site to be working on.
mylang = '{main_code}'
# The dictionary usernames should contain a username for each site where you
# have a bot account. If you have a unique username for all languages of a
# family , you can use '*'
{usernames}
# The list of BotPasswords is saved in another file. Import it if needed.
# See https://www.mediawiki.org/wiki/Manual:Pywikibot/BotPasswords to know how
# use them.
{botpasswords}
{config_text}"""
SMALL_CONFIG = """\
family = '{main_family}'
mylang = '{main_code}'
{usernames}
{botpasswords}
"""
PASSFILE_CONFIG = """\
# This is an automatically generated file used to store
# BotPasswords.
#
# As a simpler (but less secure) alternative to OAuth, MediaWiki allows bot
# users to uses BotPasswords to limit the permissions given to a bot.
# When using BotPasswords, each instance gets keys. This combination can only
# access the API, not the normal web interface.
#
# See https://www.mediawiki.org/wiki/Manual:Pywikibot/BotPasswords for more
# information.
{botpasswords}"""
[docs]def parse_sections():
"""Parse sections from config.py file.
config.py will be in the pywikibot/ directory relative to this
generate_user_files script.
:return: a list of ConfigSection named tuples.
:rtype: list
"""
data = []
ConfigSection = namedtuple('ConfigSection', 'head, info, section')
install = os.path.dirname(os.path.abspath(__file__))
with codecs.open(os.path.join(install, 'pywikibot', 'config.py'),
'r', 'utf-8') as config_f:
config_file = config_f.read()
result = re.findall(
'^(?P<section># #{5,} (?P<head>[A-Z][A-Z_ ]+[A-Z]) #{5,}\r?\n'
'(?:^#?\r?\n)?' # There may be an empty or short line after header
'(?P<comment>(?:^# .+?)+)' # first comment is used as help string
'^.*?)' # catch the remaining text
'^(?=# #{5,}|# ={5,})', # until section end marker
config_file, re.MULTILINE | re.DOTALL)
for section, head, comment in result:
info = ' '.join(text.strip('# ') for text in comment.splitlines())
data.append(ConfigSection(head, info, section))
return data
[docs]def copy_sections():
"""Take config sections and copy them to user-config.py.
:return: config text of all selected sections.
:rtype: str
"""
result = []
sections = parse_sections()
# copy settings
for section in filter(lambda x: x.head not in (DISABLED_SECTIONS
| OBSOLETE_SECTIONS),
sections):
result.append(section.section)
return ''.join(result)
[docs]def create_user_config(main_family, main_code, main_username, force=False):
"""
Create a user-config.py in base_dir.
Create a user-password.py if necessary.
"""
_fnc = os.path.join(base_dir, USER_BASENAME)
_fncpass = os.path.join(base_dir, PASS_BASENAME)
useritem = namedtuple('useritem', 'family, code, name')
userlist = []
if force and not config.verbose_output:
if main_username:
userlist = [useritem(main_family, main_code, main_username)]
else:
while True:
userlist += [useritem(*get_site_and_lang(
main_family, main_code, main_username, force=force))]
if not pywikibot.input_yn('Do you want to add any other projects?',
force=force,
default=False, automatic_quit=False):
break
# For each different username entered, ask if user wants to save a
# BotPassword (username, BotPassword name, BotPassword pass)
msg = fill('See {}/BotPasswords to know how to get codes.'
'Please note that plain text in {} and anyone with read '
'access to that directory will be able read the file.'
.format(__url__, _fncpass))
botpasswords = []
userset = {user.name for user in userlist}
for username in userset:
if pywikibot.input_yn('Do you want to add a BotPassword for {}?'
.format(username), force=force, default=False):
if msg:
pywikibot.output(msg)
msg = None
message = 'BotPassword\'s "bot name" for {}'.format(username)
botpasswordname = pywikibot.input(message, force=force)
message = 'BotPassword\'s "password" for "{}" ' \
'(no characters will be shown)' \
.format(botpasswordname)
botpasswordpass = pywikibot.input(message, force=force,
password=True)
if botpasswordname and botpasswordpass:
botpasswords.append((username, botpasswordname,
botpasswordpass))
if not userlist: # Show a sample
usernames = "# usernames['{}']['{}'] = 'MyUsername'".format(
main_family, main_code)
else:
usernames = '\n'.join(
"usernames['{user.family}']['{user.code}'] = '{user.name}'"
.format(user=user) for user in userlist)
# Arbitrarily use the first key as default settings
main_family, main_code = userlist[0].family, userlist[0].code
botpasswords = '\n'.join(
"('{}', BotPassword('{}', {!r}))".format(*botpassword)
for botpassword in botpasswords)
config_text = copy_sections()
if config_text:
config_content = EXTENDED_CONFIG
else:
pywikibot.output('Creating a small variant of user-config.py')
config_content = SMALL_CONFIG
try:
# Finally save user-config.py
with codecs.open(_fnc, 'w', 'utf-8') as f:
f.write(config_content.format(
main_family=main_family,
main_code=main_code,
usernames=usernames,
config_text=config_text,
botpasswords='password_file = ' + ('"{}"'.format(PASS_BASENAME)
if botpasswords
else 'None')))
pywikibot.output("'{}' written.".format(_fnc))
except BaseException:
if os.path.exists(_fnc):
os.remove(_fnc)
raise
save_botpasswords(botpasswords, _fncpass)
[docs]def save_botpasswords(botpasswords, _fncpass):
"""Write botpasswords to file."""
if botpasswords:
# Save user-password.py if necessary
# user-config.py is already created at this point
# therefore pywikibot.tools can be imported safely
from pywikibot.tools import file_mode_checker
try:
# First create an empty file with good permissions, before writing
# in it
with codecs.open(_fncpass, 'w', 'utf-8') as f:
f.write('')
file_mode_checker(_fncpass, mode=0o600, quiet=True)
with codecs.open(_fncpass, 'w', 'utf-8') as f:
f.write(PASSFILE_CONFIG.format(botpasswords=botpasswords))
file_mode_checker(_fncpass, mode=0o600)
pywikibot.output("'{0}' written.".format(_fncpass))
except EnvironmentError:
os.remove(_fncpass)
raise
[docs]def ask_for_dir_change(force):
"""Ask whether the base directory is has to be changed.
Only give option for directory change if user-config.py or user-password
already exists in the directory. This will repeat if user-config.py also
exists in the requested directory.
:param force: Skip asking for directory change
:type force: bool
:return: whether user file or password file exists already
:rtype: tuple of bool
"""
global base_dir
pywikibot.output('\nYour default user directory is "{}"'.format(base_dir))
while True:
# Show whether file exists
userfile = file_exists(os.path.join(base_dir, USER_BASENAME))
passfile = file_exists(os.path.join(base_dir, PASS_BASENAME))
if force and not config.verbose_output or not (userfile or passfile):
break
if pywikibot.input_yn(
'Would you like to change the directory?',
default=True, automatic_quit=False, force=force):
new_base = change_base_dir()
if new_base:
base_dir = new_base
else:
break
return userfile, passfile
[docs]def main(*args: Tuple[str, ...]):
"""
Process command line arguments and generate user-config.
If args is an empty list, sys.argv is used.
:param args: command line arguments
"""
# set the config family and mylang values to an invalid state so that
# the script can detect that the command line arguments -family & -lang
# or -site were used and handle_args has updated these config values,
# and 'force' mode can be activated below.
config.family, config.mylang = 'wikipedia', None
local_args = pywikibot.handle_args(args)
if local_args:
pywikibot.output('Unknown argument{}: {}'
.format('s' if len(local_args) > 1 else '',
', '.join(local_args)))
return
pywikibot.output('You can abort at any time by pressing ctrl-c')
if config.mylang is not None:
force = True
pywikibot.output('Automatically generating user-config.py')
else:
force = False
# Force default site of en.wikipedia
config.family, config.mylang = 'wikipedia', 'en'
username = config.usernames[config.family].get(config.mylang)
try:
has_userfile, has_passfile = ask_for_dir_change(force)
if not (has_userfile or has_passfile):
create_user_config(config.family, config.mylang, username,
force=force)
except KeyboardInterrupt:
pywikibot.output('\nScript terminated by user.')
# Creation of user-fixes.py has been replaced by an example file.
if __name__ == '__main__':
main()